diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..290c5442 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,7 @@ +[codespell] +builtin = clear,rare,informal,code,names +check-filenames = +check-hidden = +enable-colors = +ignore-words-list = sur +skip = .codespellrc,.gitattributes diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..e8ba383d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +omit = + # Omit tests + *tests/* + # Omit other files + */__init__.py + */compat.py diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index eab06c44..00000000 --- a/.coveralls.yml +++ /dev/null @@ -1,3 +0,0 @@ -service_name: travis-ci -parallel: true - diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..cc147bf0 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# setup pre-commit +a098b2819173fb56a6f479df9b29ef76d2b63d56 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8b521664 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,304 @@ +# Standard gitattributes config + +# Set the default behavior, in case people don't have core.autocrlf set. +* text eol=lf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. + +*.txt text + +# Source code +*.bash text eol=lf +*.c text +*.cpp text +*.csh text eol=lf +*.fish text eol=lf +*.inc text +*.ipynb text +*.h text +*.ksh text eol=lf +*.ps1 text +*.pxd text diff=python +*.py text diff=python +*.py3 text diff=python +*.pyi text diff=python +*.pyw text diff=python +*.pyx text diff=python +*.qss text +*.r text +*.R text +*.rb text +*.rmd text +*.Rmd text +*.rnw text +*.Rnw text +*.sh text eol=lf +*.zsh text eol=lf + +# Documentation +*.adoc text +*.latex text +*.LaTeX text +*.markdown text +*.md text +*.po text +*.pot text +*.rd text +*.Rd text +*.rst text +*.tex text +*.TeX text +*.tmpl text +*.tpl text + +# Web +*.atom text +*.css text +*.htm text +*.html text +*.js text +*.jsx text +*.json text +*.php text +*.pl text +*.rss text +*.sass text +*.scss text +*.xht text +*.xhtml text + +# Configuration +*.cfg text +*.cnf text +*.conf text +*.config text +*.desktop text +*.inf text +*.ini text +*.plist text +*.toml text +*.xml text +*.yml text +*.yaml text + +# Plain text data +*.cdl text +*.csv text +*.dif text +*.geojson text +*.gml text +*.kml text +*.sql text +*.tab text +*.tsv text +*.wkt text + +# Other text files +*.diff -text +*.patch -text + +# Special files +.*rc text +.checkignore text +.ciocheck text +.ciocopyright text +.editorconfig text +.gitattributes export-ignore +.gitconfig export-ignore +.gitignore export-ignore +.gitmodules export-ignore +.gitkeep export-ignore +*.lektorproject text +.nojekyll text +.project text + +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +INSTALL text +license text +LICENSE text +NEWS text +NOTICES text +readme text +*README* text +RELEASE text +TODO text + +browserslist text +contents.lr text +makefile text +Makefile text +MANIFEST.in text + + +# Declare files that will always have CRLF line endings on checkout. +*.bat text eol=crlf +*.cmd text eol=crlf +*.vbs text eol=crlf +*.vb text eol=crlf + + +# Denote all files that are truly binary and should not be modified. + +# Executable +*.app binary +*.bin binary +*.deb binary +*.dll binary +*.dylib binary +*.elf binary +*.exe binary +*.ko binary +*.lib binary +*.msi binary +*.o binary +*.obj binary +*.pyc binary +*.pyd binary +*.pyo binary +*.rdb binary +*.Rdb binary +*.rdx binary +*.Rdx binary +*.rpm binary +*.so binary +*.sys binary + +# Data +*.cdf binary +*.db binary +*.dta binary +*.feather binary +*.fit binary +*.fits binary +*.fts binary +*.fods binary +*.geotiff binary +*.gpkg binary +*.h4 binary +*.h5 binary +*.hdf binary +*.hdf4 binary +*.hdf5 binary +*.mat binary +*.nc binary +*.npy binary +*.npz binary +*.odb binary +*.ods binary +*.p binary +*.parquet binary +*.pickle binary +*.pkl binary +*.rdata binary +*.Rdata binary +*.RData binary +*.rda binary +*.Rda binary +*.rds binary +*.Rds binary +*.sav binary +*.sqlite binary +*.wkb binary +*.xls binary +*.XLS binary +*.xlsx binary +*.XLSX binary + +# Documents +*.doc binary +*.DOC binary +*.docx binary +*.DOCX binary +*.epub binary +*.fodp binary +*.fodt binary +*.odp binary +*.odt binary +*.pdf binary +*.PDF binary +*.ppt binary +*.PPT binary +*.pptx binary +*.PPTX binary +*.rtf binary +*.RTF binary + +# Graphics +*.ai binary +*.bmp binary +*.eps binary +*.fodg binary +*.gif binary +*.icns binary +*.ico binary +*.jp2 binary +*.jpeg binary +*.jpg binary +*.mo binary +*.pdn binary +*.png binary +*.PNG binary +*.psd binary +*.odg binary +*.svg binary +*.svgz binary +*.tif binary +*.tiff binary +*.webp binary +*.xcf binary + +# Fonts +*.eot binary +*.otc binary +*.otf binary +*.ttc binary +*.ttf binary +*.woff binary +*.woff2 binary + +# Audio/Video +*.aac binary +*.flac binary +*.mka binary +*.mkv binary +*.mp3 binary +*.mp4 binary +*.oga binary +*.ogg binary +*.ogv binary +*.opus binary +*.wav binary +*.webm binary + + +# Archives +*.7z binary +*.bz2 binary +*.dmg binary +*.gz binary +*.lz binary +*.lzma binary +*.pyz binary +*.rar binary +*.sz binary +*.tar binary +*.tbz2 binary +*.tgz binary +*.tlz binary +*.txz binary +*.xz binary +*.zip binary + +# Spyder-related +*.results binary +*.spydata binary + +# Other +*.bak binary +*.lnk binary +*.temp binary +*.tmp binary diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..f7beb14d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: spyder diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..feecd302 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,193 @@ +# Run the project's test suite +name: Tests + +on: + push: + branches: + - master + - main + - '*.x' + pull_request: + branches: + - master + - main + - '*.x' + +concurrency: + group: test-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test ${{ matrix.os }} Python ${{ matrix.python-version }} conda=${{ matrix.use-conda }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + defaults: + run: + shell: ${{ matrix.special-invocation }}bash -l {0} + env: + CI: 'True' + PYTHON_VERSION: ${{ matrix.python-version }} + USE_CONDA: ${{ matrix.use-conda }} + PYQT5_VERSION: ${{ matrix.pyqt5-version || matrix.qt5-version-default }} + PYQT5_QT_VERSION: ${{ matrix.pyqt5-qt-version || matrix.pyqt5-version || matrix.qt5-version-default }} + PYQT6_VERSION: ${{ matrix.pyqt6-version || matrix.qt6-version-default }} + PYQT6_QT_VERSION: ${{ matrix.pyqt6-qt-version || matrix.pyqt6-version || matrix.qt6-version-default }} + PYSIDE2_VERSION: ${{ matrix.pyside2-version || matrix.qt5-version-default }} + PYSIDE2_QT_VERSION: ${{ matrix.pyside2-qt-version || matrix.pyside2-version || matrix.qt5-version-default }} + PYSIDE6_VERSION: ${{ matrix.pyside6-version || matrix.qt6-version-default }} + PYSIDE6_QT_VERSION: ${{ matrix.pyside6-qt-version || matrix.pyside6-version || matrix.qt6-version-default }} + QSCINTILLA_VERSION: ${{ matrix.qscintilla-version || matrix.qscintilla-version-default }} + PYQT_EXTRAS: ${{ matrix.pyqt-extras || matrix.pyqt-extras-default }} + SKIP_PIP_CHECK: ${{ matrix.skip-pip-check }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-15-intel] + python-version: ['3.9', '3.11', '3.13'] + use-conda: ['Yes', 'No'] + qt5-version-default: ['5.12'] + qt6-version-default: ['6.5'] + qscintilla-version-default: ['2.13'] + pyqt-extras-default: ['No'] + include: + - os: ubuntu-latest + special-invocation: 'xvfb-run --auto-servernum ' # Needed for GUI tests to work + - python-version: '3.13' + pyqt5-version: '5.15' + skip-pyside2: true # Unavailable for Python 3.13+ + pyqt6-version: '6.8' + pyside6-version: '6.8' + - python-version: '3.11' + pyqt5-version: '5.15' # Python 3.11 needs 5.15+ + pyside2-version: '5.15' # Python 3.11 needs 5.15+ + pyside6-version: '6.8' # Python 3.11 needs 6.4+. Test upper bound + - use-conda: 'Yes' + skip-pyqt6: true # PyQt6 conda packages only available from anaconda channel + - use-conda: 'No' + pyqt5-version: '5.15' # Test with latest optional packages + - python-version: '3.9' + use-conda: 'Yes' + pyside2-version: '5.13' # Conda needs 5.13+ to work reliably + pyside2-qt-version: '5.12' # Conda only has 5.12 and 5.15, not 5.13 + - python-version: '3.11' + use-conda: 'No' + pyqt-extras: 'Yes' # Check PyQt extras + skip-pyside2: true # Pyside2 wheels don't support Python 3.11+ + pyqt6-version: '6.8' # Test upper bound + pyside6-version: '6.8' # Test upper bound + - os: windows-latest + python-version: '3.9' + use-conda: 'No' + pyqt6-version: 6.2 # Test lower bound + pyside6-version: 6.2 # Test lower bound + pyside2-version: 5.15 # Version 5.12 not available + - os: windows-latest + python-version: '3.11' + use-conda: 'Yes' + pyside6-version: 6.8 # Test upper bound + - os: macos-15-intel + python-version: '3.11' + use-conda: 'No' + pyqt6-version: 6.8 # Test upper bound + pyside2-version: 5.15 # Test upper bound + - os: macos-15-intel + python-version: '3.9' + use-conda: 'No' + pyqt6-version: 6.2 # Test lower bound + pyside6-version: 6.2 # Test lower bound + - os: macos-15-intel + python-version: '3.9' + use-conda: 'Yes' + pyside6-version: 6.8 # Test upper bound. Gets stuck with 6.5 + exclude: + - os: macos-15-intel + python-version: '3.11' + use-conda: 'Yes' + steps: + - name: Check job values + run: | + echo "---- General setup" + echo "OS:" ${{ matrix.os }} + echo "PYTHON_VERSION:" ${{ env.PYTHON_VERSION }} + echo "USE_CONDA:" ${{ env.USE_CONDA }} + echo "---- PyQt" + echo "PYQT_EXTRAS:" ${{ env.PYQT_EXTRAS }} + echo "---- PyQt5" + echo "SKIP_PYQT5:" ${{ matrix.skip-pyqt5 }} + echo "PYQT5_VERSION:" ${{ env.PYQT5_VERSION }} + echo "PYQT5_QT_VERSION:" ${{ env.PYQT5_QT_VERSION }} + echo "QSCINTILLA_VERSION:" ${{ env.QSCINTILLA_VERSION }} + echo "---- PyQt6" + echo "SKIP_PYQT6:" ${{ matrix.skip-pyqt6 }} + echo "PYQT6_VERSION:" ${{ env.PYQT6_VERSION }} + echo "PYQT6_QT_VERSION:" ${{ env.PYQT6_QT_VERSION }} + echo "---- PySide2" + echo "SKIP_PYSIDE2:" ${{ matrix.skip-pyside2 }} + echo "PYSIDE2_VERSION:" ${{ env.PYSIDE2_VERSION }} + echo "PYSIDE2_QT_VERSION:" ${{ env.PYSIDE2_QT_VERSION }} + echo "---- PySide6" + echo "SKIP_PYSIDE6:" ${{ matrix.skip-pyside6 }} + echo "PYSIDE6_VERSION:" ${{ env.PYSIDE6_VERSION }} + echo "PYSIDE6_QT_VERSION:" ${{ env.PYSIDE6_QT_VERSION }} + echo "---- Other" + echo "SKIP_PIP_CHECK:" ${{ env.SKIP_PIP_CHECK }} + - name: Checkout branch + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install Linux system packages + if: contains(matrix.os, 'ubuntu') + shell: bash + run: | + sudo apt update + sudo apt install libpulse-dev libegl1-mesa-dev libopengl0 gstreamer1.0-gl + - uses: tlambert03/setup-qt-libs@v1 + - name: Install Conda + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: '' + auto-activate-base: true + auto-update-conda: true + channels: conda-forge + conda-remove-defaults: true + channel-priority: strict + miniforge-version: latest + - name: Print Conda info + shell: bash -el {0} + run: | + conda info + conda list + - name: Test PyQt5 + if: (! matrix.skip-pyqt5) + run: ./.github/workflows/test.sh pyqt5 + - name: Test PyQt6 + if: always() && (! (matrix.skip-pyqt6)) + run: ./.github/workflows/test.sh pyqt6 + - name: Test PySide2 + if: always() && (! (matrix.skip-pyside2)) + run: ./.github/workflows/test.sh pyside2 + - name: Test PySide6 + if: always() && (! (matrix.skip-pyside6)) + run: ./.github/workflows/test.sh pyside6 + - name: Upload coverage data to coveralls.io + uses: coverallsapp/github-action@v2 + with: + parallel: true + flag-name: ${{ matrix.os }} Python ${{ matrix.python-version }} conda=${{ matrix.use-conda }} + github-token: ${{ secrets.GITHUB_TOKEN }} + debug: true + fail-on-error: false + finish: + name: Finish Coveralls + needs: test + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Mark Coveralls finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + fail-on-error: false diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh new file mode 100755 index 00000000..1e25131a --- /dev/null +++ b/.github/workflows/test.sh @@ -0,0 +1,109 @@ +#!/bin/bash -ex + +eval "$(conda shell.bash hook)" + +# Create and activate conda environment for this test +BINDING=$(echo "$1" | tr '[:lower:]' '[:upper:]') +QT_VERSION_VAR=${BINDING}_QT_VERSION + +# pytest-qt >=4.5.0 doesn't support PySide2 +if [ "${1}" = "pyside2" ]; then + PYTESTQT_VERSION="<4.5.0" +fi + +# pytest-qt >=4 doesn't support Qt <=5.9 +if [ "${!QT_VERSION_VAR:0:3}" = "5.9" ]; then + PYTESTQT_VERSION="=3.3.0" + PYTEST_VERSION=">=6,!=7.0.0,!=7.0.1,<7.2.0" +fi + + +if [ "$USE_CONDA" = "Yes" ]; then + + if [ "${1}" = "pyqt5" ]; then + QT_SPECS="qt=${PYQT5_QT_VERSION} pyqt=${PYQT5_VERSION}" + elif [ "${1}" = "pyside2" ]; then + QT_SPECS="qt=${PYSIDE2_QT_VERSION} pyside2=${PYSIDE2_VERSION}" + elif [ "${1}" = "pyside6" ]; then + QT_SPECS="qt6-main=${PYSIDE6_QT_VERSION} pyside6=${PYSIDE6_VERSION}" + if [ "${!QT_VERSION_VAR:0:3}" = "6.8" ]; then + QT_SPECS+=" qt6-multimedia=${PYSIDE6_QT_VERSION}" + fi + else + exit 1 + fi + +fi + +conda create -y -n test-env-${BINDING} python=${PYTHON_VERSION} pytest${PYTEST_VERSION:->=6,!=7.0.0,!=7.0.1} pytest-cov>=3.0.0 pytest-qt${PYTESTQT_VERSION:-} ${QT_SPECS:-} + +conda activate test-env-${BINDING} + +if [ "$USE_CONDA" = "No" ]; then + + if [ "${1}" = "pyqt5" ]; then + + if [ "$PYQT_EXTRAS" = "Yes" ]; then + pip install pyqt5==${PYQT5_VERSION}.* \ + PyQtWebEngine==${PYQT5_VERSION}.* \ + QScintilla==${QSCINTILLA_VERSION}.* \ + PyQt3D==${PYQT5_VERSION}.* \ + PyQtChart==${PYQT5_VERSION}.* \ + PyQtDataVisualization==${PYQT5_VERSION}.* \ + PyQtNetworkAuth==${PYQT5_VERSION}.* \ + PyQtPurchasing==${PYQT5_VERSION}.* + else + pip install pyqt5==${PYQT5_VERSION}.* \ + PyQtWebEngine==${PYQT5_VERSION}.* \ + QScintilla==${QSCINTILLA_VERSION}.* + fi + + elif [ "${1}" = "pyqt6" ]; then + + if [ "$PYQT_EXTRAS" = "Yes" ]; then + pip install pyqt6==${PYQT6_VERSION}.* \ + PyQt6-WebEngine==${PYQT6_VERSION}.* \ + PyQt6-Qt6==${PYQT6_QT_VERSION}.* \ + PyQt6-QScintilla \ + PyQt6-3D==${PYQT6_VERSION}.* \ + PyQt6-Charts==${PYQT6_VERSION}.* \ + PyQt6-DataVisualization==${PYQT6_VERSION}.* \ + PyQt6-NetworkAuth==${PYQT6_VERSION}.* + else + pip install pyqt6==${PYQT6_VERSION}.* \ + PyQt6-WebEngine==${PYQT6_VERSION}.* \ + PyQt6-Qt6==${PYQT6_QT_VERSION}.* + fi + + elif [ "${1}" = "pyside2" ]; then + pip install pyside2==${PYSIDE2_VERSION}.* + elif [ "${1}" = "pyside6" ]; then + if [ "${PYSIDE6_VERSION:0:3}" = "6.2" ]; then + pip install pyside6==${PYSIDE6_VERSION}.* + else + pip install pyside6==${PYSIDE6_VERSION}.* pyside6-addons==${PYSIDE6_VERSION}.* pyside6-essentials==${PYSIDE6_VERSION}.* + fi + else + exit 1 + fi + +fi + +# Build wheel of package +git clean -xdf -e *.coverage +python -m pip install --upgrade pip +python -m pip install --upgrade build +python -bb -X dev -W error -m build + +# Install package from built wheel +echo dist/*.whl | xargs -I % python -bb -X dev -W error -W "ignore::DeprecationWarning:pip._internal.locations._distutils" -W "ignore::DeprecationWarning:distutils.command.install" -W "ignore::DeprecationWarning:pip._internal.metadata.importlib._envs" -m pip install --upgrade % + +# Print environment information +conda list + +# Run tests +python -I -bb -X dev -W error -m pytest --cov qtpy --cov-config .coveragerc --cov-append + +# Check package and environment +pipx run twine check --strict dist/* +pip check -v || ${SKIP_PIP_CHECK:-false} diff --git a/.gitignore b/.gitignore index 6def0a77..26e37b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,171 @@ -# Python compiled files -*.py[ocd] +# Standard gitignore list -# C extensions -*.so +# Archive files +*.7z +*.bz2 +*.gz +*.lzma +*.lzma2 +*.tar +*.xz +*.zip -# Kate -.directory +# Temporary / backup files +*.bak +*.swp +*.tmp +*.temp -# PyPi configuration -.pypirc +# OS-specific files +.DS_Store +Thumbs.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so -# Packages +# Distribution / packaging *.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs +*.egg-info/ +.eggs/ .installed.cfg -lib -lib64 +.Python +bin/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +MANIFEST +parts/ +pip-wheel-metadata/ +sdist/ +share/python-wheels/ +var/ +wheels/ + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +.cache +*.cover +.coverage +.coverage.* +.hypothesis/ +.nox/ +.pytest_cache/ +.tox/ +coverage.xml +htmlcov/ +nosetests.xml # Translations *.mo -# Gedit files +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +doc/_build/ +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# Celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +ENV/ +venv/ +VENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject +spyder_crash.log + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Pylint +.pylint.d/ + +# gedit files *~ +# Notepad++ files +nppBackup/ + +# git .orig files +*.orig + +# IDEA project settings +.idea/ + +# VSCode files +.vscode/ + +# Kate +.directory +.swp.* +*.swp + +# PyPi configuration +.pypirc + # Other files toread.md -.chache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c72a5261 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +repos: +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.11.0 + hooks: + - id: black + pass_filenames: true + exclude: _vendor|vendored|examples +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 + hooks: + - id: ruff + exclude: _vendor|vendored +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.27.2 + hooks: + - id: check-github-workflows +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-json + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: .*\.md + - id: debug-statements + - id: mixed-line-ending + +ci: + autoupdate_schedule: monthly diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bc3c5323..00000000 --- a/.travis.yml +++ /dev/null @@ -1,53 +0,0 @@ -# https://travis-ci.org/spyder-ide/qtpy/ - -# We set the language to c because python isn't supported on the MacOS X nodes -# on Travis. However, the language ends up being irrelevant anyway, since we -# install Python ourselves using conda. -language: c - -sudo: false - -branches: - only: - - master - -os: - - linux - - osx - -env: - global: - - SETUP_XVFB=True - - CONDA_CHANNELS="qttesting" - - CONDA_DEPENDENCIES="pytest pytest-cov" - - PIP_DEPENDENCIES="coveralls" - - matrix: - - PYTHON_VERSION=2.7 USE_QT_API=PyQt5 - - PYTHON_VERSION=2.7 USE_QT_API=PyQt4 - - PYTHON_VERSION=2.7 USE_QT_API=PySide - - PYTHON_VERSION=3.4 USE_QT_API=PyQt4 - - PYTHON_VERSION=3.5 USE_QT_API=PyQt4 - - PYTHON_VERSION=3.5 USE_QT_API=PyQt5 - -before_install: - # Test environments for different Qt bindings - - if [[ "$USE_QT_API" == "PyQt5" ]]; then - export CONDA_DEPENDENCIES='qt=5.* pyqt=5.* '$CONDA_DEPENDENCIES; - elif [[ "$USE_QT_API" == "PyQt4" ]]; then - export CONDA_DEPENDENCIES='qt=4.* pyqt=4.* '$CONDA_DEPENDENCIES; - elif [[ "$USE_QT_API" == "PySide" ]]; then - export CONDA_DEPENDENCIES='qt=4.* pyside '$CONDA_DEPENDENCIES; - fi - -install: - - git clone git://github.com/astropy/ci-helpers.git - - source ci-helpers/travis/setup_conda_$TRAVIS_OS_NAME.sh - - python setup.py install - -script: - - python qtpy/tests/runtests.py - -after_success: - - coveralls - diff --git a/AUTHORS.md b/AUTHORS.md index a6b4d15e..462024b5 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,16 +1,21 @@ -Maintainer -========== +# Authors -Gonzalo Peña-Castellanos ([@goanpeca](http://github.com/goanpeca)) -Main Authors -============ +## Original Authors -* Colin Duquesnoy ([@ColinDuquesnoy](http://github.com/ColinDuquesnoy)) +* [pyqode.qt](https://github.com/pyQode/pyqode.qt): Colin Duquesnoy ([@ColinDuquesnoy](https://github.com/ColinDuquesnoy)) +* [spyderlib.qt](https://github.com/spyder-ide/spyder/commits/2.3/spyderlib/qt): Pierre Raybaut ([@PierreRaybaut](https://github.com/PierreRaybaut)) +* [qt-helpers](https://github.com/glue-viz/qt-helpers): Thomas Robitaille ([@astrofrog](https://www.github.com/astrofrog)) -* [The Spyder Development Team](https://github.com/spyder-ide/spyder/graphs/contributors) -Contributors -============ +## Current Maintainers -* Thomas Robitaille ([@astrofrog](http://www.github.com/astrofrog)) \ No newline at end of file +* Daniel Althviz ([@dalthviz](https://github.com/dalthviz)) +* Carlos Cordoba ([@ccordoba12](https://github.com/ccordoba12)) +* C.A.M. Gerlach ([@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* Spyder Development Team ([Spyder-IDE](https://github.com/spyder-ide)) + + +## Contributors + +* [The QtPy Contributors](https://github.com/spyder-ide/qtpy/graphs/contributors) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0cef2b..c50fbff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,874 @@ # History of changes +## Version 2.4.3 (2025-02-11) + +### Issues Closed + +* [Issue 509](https://github.com/spyder-ide/qtpy/issues/509) - Release 2.4.3 +* [Issue 505](https://github.com/spyder-ide/qtpy/issues/505) - Breaking change related to QMenu in 2.4.2 ([PR 507](https://github.com/spyder-ide/qtpy/pull/507) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 502](https://github.com/spyder-ide/qtpy/issues/502) - Hijacked PyQt types are not returned from native Qt APIs ([PR 507](https://github.com/spyder-ide/qtpy/pull/507) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 501](https://github.com/spyder-ide/qtpy/issues/501) - Check/fix several CI warnings ([PR 511](https://github.com/spyder-ide/qtpy/pull/511) by [@dalthviz](https://github.com/dalthviz)) + +In this release 4 issues were closed. + +### Pull Requests Merged + +* [PR 511](https://github.com/spyder-ide/qtpy/pull/511) - PR: Remove `Mambaforge` usage and update Linux setup (CI), by [@dalthviz](https://github.com/dalthviz) ([501](https://github.com/spyder-ide/qtpy/issues/501)) +* [PR 507](https://github.com/spyder-ide/qtpy/pull/507) - PR: Ensure `QMenu` and `QToolBar` `isinstance` checks succeed and remove unneeded wrapper classes , by [@dalthviz](https://github.com/dalthviz) ([505](https://github.com/spyder-ide/qtpy/issues/505), [502](https://github.com/spyder-ide/qtpy/issues/502)) +* [PR 506](https://github.com/spyder-ide/qtpy/pull/506) - PR: Ignore PySide6 `QSqlDatabase.exec` `DeprecationWarning`, by [@juliangilbey](https://github.com/juliangilbey) + +In this release 3 pull requests were closed. + + +---- + + +## Version 2.4.2 (2024-11-04) + +### Issues Closed + +* [Issue 498](https://github.com/spyder-ide/qtpy/issues/498) - Release 2.4.2 +* [Issue 485](https://github.com/spyder-ide/qtpy/issues/485) - Update tests to be compatible with pytest 8.2 ([PR 486](https://github.com/spyder-ide/qtpy/pull/486) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 426](https://github.com/spyder-ide/qtpy/issues/426) - Coveralls config needs to be updated ([PR 428](https://github.com/spyder-ide/qtpy/pull/428) by [@dalthviz](https://github.com/dalthviz)) + +In this release 3 issues were closed. + +### Pull Requests Merged + +* [PR 497](https://github.com/spyder-ide/qtpy/pull/497) - Replace Quansight logo by CZI one in Readme, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 495](https://github.com/spyder-ide/qtpy/pull/495) - PR: Help users understand why PySide 6.8.0 gives a segfault, by [@hmaarrfk](https://github.com/hmaarrfk) +* [PR 486](https://github.com/spyder-ide/qtpy/pull/486) - PR: Update `pytest.importorskip` usage (CI), by [@dalthviz](https://github.com/dalthviz) ([485](https://github.com/spyder-ide/qtpy/issues/485)) +* [PR 483](https://github.com/spyder-ide/qtpy/pull/483) - PR: Fix coverage errors and macOS jobs, by [@dalthviz](https://github.com/dalthviz) +* [PR 475](https://github.com/spyder-ide/qtpy/pull/475) - PR: Add `QtCore.Qt.MouseButton.MidButton` alias for Qt6, by [@PierreRaybaut](https://github.com/PierreRaybaut) +* [PR 471](https://github.com/spyder-ide/qtpy/pull/471) - [pre-commit.ci] pre-commit autoupdate, by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci) +* [PR 468](https://github.com/spyder-ide/qtpy/pull/468) - [pre-commit.ci] pre-commit autoupdate, by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci) +* [PR 461](https://github.com/spyder-ide/qtpy/pull/461) - PR: Make `QAction.setShortcut` and `setShortcuts` accept many types, by [@StSav012](https://github.com/StSav012) +* [PR 428](https://github.com/spyder-ide/qtpy/pull/428) - PR: Update coveralls config (CI), by [@dalthviz](https://github.com/dalthviz) ([426](https://github.com/spyder-ide/qtpy/issues/426)) + +In this release 9 pull requests were closed. + + +---- + + +## Version 2.4.1 (2023-10-23) + +### Issues Closed + +* [Issue 462](https://github.com/spyder-ide/qtpy/issues/462) - Release QtPy 2.4.1 +* [Issue 458](https://github.com/spyder-ide/qtpy/issues/458) - Typo causes failed QWebEngineScript with PySide6 + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 459](https://github.com/spyder-ide/qtpy/pull/459) - [pre-commit.ci] pre-commit autoupdate, by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci) +* [PR 456](https://github.com/spyder-ide/qtpy/pull/456) - [pre-commit.ci] pre-commit autoupdate, by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci) +* [PR 455](https://github.com/spyder-ide/qtpy/pull/455) - PR: Fix bug when importing from `PySide6.QtWebEngineCore/QtWebEngineWidgets` (`QWebEngineScrip` vs `QWebEngineScript`) , by [@damonlynch](https://github.com/damonlynch) + +In this release 3 pull requests were closed. + + +---- + + +## Version 2.4.0 (2023-08-29) + +### Issues Closed + +* [Issue 453](https://github.com/spyder-ide/qtpy/issues/453) - Release QtPy 2.4.0 +* [Issue 447](https://github.com/spyder-ide/qtpy/issues/447) - Type hints not working for `qtpy` specific imports ([PR 450](https://github.com/spyder-ide/qtpy/pull/450) by [@wkrasnicki](https://github.com/wkrasnicki)) +* [Issue 442](https://github.com/spyder-ide/qtpy/issues/442) - Some compatibility issues when using latest PyQt6 or PySide6 (6.5+) ([PR 448](https://github.com/spyder-ide/qtpy/pull/448) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 439](https://github.com/spyder-ide/qtpy/issues/439) - qtpy.uic.loadUiType failing for PySide6 ([PR 440](https://github.com/spyder-ide/qtpy/pull/440) by [@JaRoSchm](https://github.com/JaRoSchm)) +* [Issue 432](https://github.com/spyder-ide/qtpy/issues/432) - Add compatibility layer for keywords arguments of `QFileDialog ` `get*` class methods ([PR 433](https://github.com/spyder-ide/qtpy/pull/433) by [@Czaki](https://github.com/Czaki)) +* [Issue 423](https://github.com/spyder-ide/qtpy/issues/423) - Fix flaky importlib error on Ubuntu Python 3.7 conda CI job ([PR 425](https://github.com/spyder-ide/qtpy/pull/425) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 383](https://github.com/spyder-ide/qtpy/issues/383) - Add extra packages available for PyQt to CI for testing ([PR 446](https://github.com/spyder-ide/qtpy/pull/446) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 345](https://github.com/spyder-ide/qtpy/issues/345) - Use pre-commit to automatically format code with Black and imports with isort ([PR 451](https://github.com/spyder-ide/qtpy/pull/451) by [@Czaki](https://github.com/Czaki)) + +In this release 8 issues were closed. + +### Pull Requests Merged + +* [PR 451](https://github.com/spyder-ide/qtpy/pull/451) - PR: Setup pre-commit and format all code with Black, by [@Czaki](https://github.com/Czaki) ([345](https://github.com/spyder-ide/qtpy/issues/345)) +* [PR 450](https://github.com/spyder-ide/qtpy/pull/450) - PR: Update README with information on Pyright/Pylance integration and add CLI subcommand to help generate the related config, by [@wkrasnicki](https://github.com/wkrasnicki) ([447](https://github.com/spyder-ide/qtpy/issues/447)) +* [PR 449](https://github.com/spyder-ide/qtpy/pull/449) - PR: Improve enum to flags aliasing for PyQt6 and PySide6 > 6.3 and `QFileDialog` static methods kwarg compatibility, by [@dalthviz](https://github.com/dalthviz) +* [PR 448](https://github.com/spyder-ide/qtpy/pull/448) - PR: Restore `QtWidgets.QFileDialog.Options` access as `QtWidgets.QFileDialog.Option` alias (PyQt6), by [@dalthviz](https://github.com/dalthviz) ([442](https://github.com/spyder-ide/qtpy/issues/442)) +* [PR 446](https://github.com/spyder-ide/qtpy/pull/446) - PR: Test using `PyQt` extra packages, by [@dalthviz](https://github.com/dalthviz) ([383](https://github.com/spyder-ide/qtpy/issues/383)) +* [PR 445](https://github.com/spyder-ide/qtpy/pull/445) - PR: Add mappings for deprecated `QDropEvent` `pos` and `posF` methods, by [@dalthviz](https://github.com/dalthviz) +* [PR 444](https://github.com/spyder-ide/qtpy/pull/444) - PR: Restore `Qt.ItemFlags` access as `Qt.ItemFlag` alias (PyQt6), by [@dalthviz](https://github.com/dalthviz) +* [PR 443](https://github.com/spyder-ide/qtpy/pull/443) - PR: Update bindings upper bound version to 6.5 and ignore `DeprecationWarning` (CI), by [@dalthviz](https://github.com/dalthviz) +* [PR 440](https://github.com/spyder-ide/qtpy/pull/440) - PR: Replace custom implementation with loadUiType from PySide6, by [@JaRoSchm](https://github.com/JaRoSchm) ([439](https://github.com/spyder-ide/qtpy/issues/439)) +* [PR 438](https://github.com/spyder-ide/qtpy/pull/438) - PR: Make `exec()` for PySide2, by [@StSav012](https://github.com/StSav012) +* [PR 437](https://github.com/spyder-ide/qtpy/pull/437) - PR: Make `QMenu.addAction` and `QToolBar.addAction` compatible with Qt6 arguments' order, by [@StSav012](https://github.com/StSav012) +* [PR 434](https://github.com/spyder-ide/qtpy/pull/434) - PR: Enable more tests on CI, by [@Czaki](https://github.com/Czaki) +* [PR 433](https://github.com/spyder-ide/qtpy/pull/433) - PR: Add wrapers to fix argument name in `QFileDialog` `get*`methods, by [@Czaki](https://github.com/Czaki) ([432](https://github.com/spyder-ide/qtpy/issues/432)) +* [PR 431](https://github.com/spyder-ide/qtpy/pull/431) - PR: Rename `utils.py` to `_utils.py` to make it private, by [@StSav012](https://github.com/StSav012) +* [PR 425](https://github.com/spyder-ide/qtpy/pull/425) - PR: Make CI jobs much faster & less flaky w/Mamba, 1-pass solve & other tweaks, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([423](https://github.com/spyder-ide/qtpy/issues/423)) +* [PR 424](https://github.com/spyder-ide/qtpy/pull/424) - PR: Add `QEnum` macro for PyQt bindings, by [@phil65](https://github.com/phil65) +* [PR 422](https://github.com/spyder-ide/qtpy/pull/422) - PR: Use static calls of `exec_` elsewhere where needed, and test them, by [@StSav012](https://github.com/StSav012) +* [PR 421](https://github.com/spyder-ide/qtpy/pull/421) - PR: Symmetrize `QDateTime.toPython` and `toPyDateTime`, etc., by [@StSav012](https://github.com/StSav012) +* [PR 420](https://github.com/spyder-ide/qtpy/pull/420) - PR: Symmetrize `path` and `location` of `QLibraryInfo`, by [@StSav012](https://github.com/StSav012) +* [PR 419](https://github.com/spyder-ide/qtpy/pull/419) - PR: Add missing issue closed for v2.3.1 changelog entry, by [@dalthviz](https://github.com/dalthviz) +* [PR 387](https://github.com/spyder-ide/qtpy/pull/387) - PR: Improve import modularity between `QtGui`, `QtWidgets` and `QtOpenGL*` related modules, by [@DaelonSuzuka](https://github.com/DaelonSuzuka) + +In this release 21 pull requests were closed. + + +---- + + +## Version 2.3.1 (2023-03-28) + +### Issues Closed + +* [Issue 416](https://github.com/spyder-ide/qtpy/issues/416) - Release QtPy 2.3.1 +* [Issue 412](https://github.com/spyder-ide/qtpy/issues/412) - How to catch QtBindingsNotFoundError ([PR 413](https://github.com/spyder-ide/qtpy/pull/413) by [@cbrnr](https://github.com/cbrnr)) +* [Issue 405](https://github.com/spyder-ide/qtpy/issues/405) - Remove patch that "may be limited to `PySide-5.11a1` only" +* [Issue 402](https://github.com/spyder-ide/qtpy/issues/402) - `QTextStreamManipulator` has no `exec` method on PyQt5/6 +* [Issue 394](https://github.com/spyder-ide/qtpy/issues/394) - Differences in QEvent subclass APIs in PyQt6 cause attribute and/or type errors +* [Issue 390](https://github.com/spyder-ide/qtpy/issues/390) - QtBindingsNotFoundError is not raised correctly (from None) in __init__ ([PR 391](https://github.com/spyder-ide/qtpy/pull/391) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 389](https://github.com/spyder-ide/qtpy/issues/389) - Make `QtWidgets` and `QtGui` modules compatible with `PySide6`/`PyQt6` import locations for `PySide2`/`PyQt5` ([PR 410](https://github.com/spyder-ide/qtpy/pull/410) by [@StSav012](https://github.com/StSav012)) +* [Issue 386](https://github.com/spyder-ide/qtpy/issues/386) - Add official support for Python 3.11 ([PR 392](https://github.com/spyder-ide/qtpy/pull/392) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) + +In this release 8 issues were closed. + +### Pull Requests Merged + +* [PR 417](https://github.com/spyder-ide/qtpy/pull/417) - PR: Add compatibility mappings between bindings for all children of `QSinglePointEvent`, by [@StSav012](https://github.com/StSav012) +* [PR 414](https://github.com/spyder-ide/qtpy/pull/414) - PR: Skip QtPositioning tests on Conda Qt >=6.4.3 where its not included, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 413](https://github.com/spyder-ide/qtpy/pull/413) - PR: Make `QtBindingsNotFoundError` also inherit from `ImportError`, by [@cbrnr](https://github.com/cbrnr) ([412](https://github.com/spyder-ide/qtpy/issues/412)) +* [PR 410](https://github.com/spyder-ide/qtpy/pull/410) - PR: Improve compatibility for `QtWidgets` and `QtGui` modules between Qt5 and Qt6 bindings, by [@StSav012](https://github.com/StSav012) +* [PR 408](https://github.com/spyder-ide/qtpy/pull/408) - PR: Add mappings for QMouseEvent methods, by [@StSav012](https://github.com/StSav012) +* [PR 407](https://github.com/spyder-ide/qtpy/pull/407) - PR: Remove patch that may be limited to PySide-5.11a1 only, by [@StSav012](https://github.com/StSav012) +* [PR 404](https://github.com/spyder-ide/qtpy/pull/404) - PR: Don't make `QTextStreamManipulator.exec_` on PyQt5/6, by [@StSav012](https://github.com/StSav012) +* [PR 401](https://github.com/spyder-ide/qtpy/pull/401) - PR: Unskip PySide2 5.15 on Python 3.11 with Conda on CIs, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 398](https://github.com/spyder-ide/qtpy/pull/398) - PR: Make warning usage consistant and refine messages, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 397](https://github.com/spyder-ide/qtpy/pull/397) - Simplify CI script following Qt 5.15.6 feedstock update, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 393](https://github.com/spyder-ide/qtpy/pull/393) - PR: Fix `LibraryLocation` -> `LibraryPath` renaming due to deprecation with Qt6, by [@StSav012](https://github.com/StSav012) +* [PR 392](https://github.com/spyder-ide/qtpy/pull/392) - PR: Add Python 3.11 to CIs/tags & overhaul CI config, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([386](https://github.com/spyder-ide/qtpy/issues/386)) +* [PR 391](https://github.com/spyder-ide/qtpy/pull/391) - PR: Use raise from None when raising QtBindingsNotFoundError in __init__, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([390](https://github.com/spyder-ide/qtpy/issues/390)) + +In this release 13 pull requests were closed. + + +---- + + +## Version 2.3.0 (2022-11-07) + +### Issues Closed + +* [Issue 384](https://github.com/spyder-ide/qtpy/issues/384) - Release QtPy 2.3.0 +* [Issue 381](https://github.com/spyder-ide/qtpy/issues/381) - Add QtPdf, QtPdfWidgets ([PR 382](https://github.com/spyder-ide/qtpy/pull/382) by [@jschueller](https://github.com/jschueller)) +* [Issue 375](https://github.com/spyder-ide/qtpy/issues/375) - Test PySide6 6.4.x ([PR 376](https://github.com/spyder-ide/qtpy/pull/376) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 373](https://github.com/spyder-ide/qtpy/issues/373) - PySide6 6.4.0 support - `AttributeError: Cannot reassign members` when aliasing enum values ([PR 374](https://github.com/spyder-ide/qtpy/pull/374) by [@astrofrog](https://github.com/astrofrog)) +* [Issue 367](https://github.com/spyder-ide/qtpy/issues/367) - Import behaviour if no Qt bindings are installed ([PR 379](https://github.com/spyder-ide/qtpy/pull/379) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 134](https://github.com/spyder-ide/qtpy/issues/134) - QScintilla Support ([PR 372](https://github.com/spyder-ide/qtpy/pull/372) by [@dgoeries](https://github.com/dgoeries)) + +In this release 6 issues were closed. + +### Pull Requests Merged + +* [PR 382](https://github.com/spyder-ide/qtpy/pull/382) - PR: Add `QtPdf` and `QtPdfWidgets`, by [@jschueller](https://github.com/jschueller) ([381](https://github.com/spyder-ide/qtpy/issues/381)) +* [PR 380](https://github.com/spyder-ide/qtpy/pull/380) - PR: Enable more qt6 tests, by [@jschueller](https://github.com/jschueller) +* [PR 379](https://github.com/spyder-ide/qtpy/pull/379) - PR: Raise error when no bindings are found at `__init__`, by [@dalthviz](https://github.com/dalthviz) ([367](https://github.com/spyder-ide/qtpy/issues/367)) +* [PR 378](https://github.com/spyder-ide/qtpy/pull/378) - PR: Try PySide6 on conda, by [@jschueller](https://github.com/jschueller) +* [PR 376](https://github.com/spyder-ide/qtpy/pull/376) - PR: Expand the CI test matrix with Qt/bindings 6.4 and fix tests with PyQt 5.9, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([375](https://github.com/spyder-ide/qtpy/issues/375)) +* [PR 374](https://github.com/spyder-ide/qtpy/pull/374) - Don't re-assign Qt.MouseButton.MiddleButton on PySide6, by [@astrofrog](https://github.com/astrofrog) ([373](https://github.com/spyder-ide/qtpy/issues/373)) +* [PR 372](https://github.com/spyder-ide/qtpy/pull/372) - PR: Add `Qsci` to the imports, by [@dgoeries](https://github.com/dgoeries) ([134](https://github.com/spyder-ide/qtpy/issues/134)) + +In this release 7 pull requests were closed. + + +---- + + +## Version 2.2.1 (2022-10-03) + +### Issues Closed + +* [Issue 369](https://github.com/spyder-ide/qtpy/issues/369) - Release QtPy 2.2.1 +* [Issue 365](https://github.com/spyder-ide/qtpy/issues/365) - Additions needed for PySide6/PyQt6 support (`QFontMetricsF.width`, `QLineEdit.getTextMargins` and `QtWidgets.QUndoCommand`) ([PR 366](https://github.com/spyder-ide/qtpy/pull/366) by [@random-developer](https://github.com/random-developer)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 368](https://github.com/spyder-ide/qtpy/pull/368) - PR: Add missing `self` for `QtBindingsNotFoundError` definition, by [@dalthviz](https://github.com/dalthviz) +* [PR 366](https://github.com/spyder-ide/qtpy/pull/366) - PR: Aliased `QUndoCommand`, provided `QLineEdit.getTextMargins()` and `QFontMetricsF.width()`, by [@random-developer](https://github.com/random-developer) ([365](https://github.com/spyder-ide/qtpy/issues/365)) +* [PR 363](https://github.com/spyder-ide/qtpy/pull/363) - PR: Fix typo in Qt flags in Readme, by [@Czaki](https://github.com/Czaki) +* [PR 362](https://github.com/spyder-ide/qtpy/pull/362) - PR: Fix various minor typos found with Codespell, by [@luzpaz](https://github.com/luzpaz) +* [PR 361](https://github.com/spyder-ide/qtpy/pull/361) - PR: toPython helpers for QDate and QTime, by [@bob-schumaker](https://github.com/bob-schumaker) +* [PR 360](https://github.com/spyder-ide/qtpy/pull/360) - CI: Ensure conda-forge channel usage when testing with conda, by [@dalthviz](https://github.com/dalthviz) + +In this release 6 pull requests were closed. + + +---- + + +## Version 2.2.0 (2022-08-10) + +### Issues Closed + +* [Issue 359](https://github.com/spyder-ide/qtpy/issues/359) - Release QtPy 2.2.0 +* [Issue 352](https://github.com/spyder-ide/qtpy/issues/352) - Deprecation Warning for Enum Access ([PR 353](https://github.com/spyder-ide/qtpy/pull/353) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 351](https://github.com/spyder-ide/qtpy/issues/351) - `PySide6.QtSvgWidgets` not exposed +* [Issue 302](https://github.com/spyder-ide/qtpy/issues/302) - Compat shiboken and sip like Qt.py ([PR 354](https://github.com/spyder-ide/qtpy/pull/354) by [@zjp](https://github.com/zjp)) +* [Issue 61](https://github.com/spyder-ide/qtpy/issues/61) - Add documentation for methods or helpers that are specific to qtpy ([PR 357](https://github.com/spyder-ide/qtpy/pull/357) by [@dalthviz](https://github.com/dalthviz)) + +In this release 5 issues were closed. + +### Pull Requests Merged + +* [PR 358](https://github.com/spyder-ide/qtpy/pull/358) - PR: Fix PyQt6 typing import for Qt, by [@tlambert03](https://github.com/tlambert03) +* [PR 357](https://github.com/spyder-ide/qtpy/pull/357) - PR: Add initial `Methods, helpers and QtPy namespace specifics` section to the README, by [@dalthviz](https://github.com/dalthviz) ([61](https://github.com/spyder-ide/qtpy/issues/61)) +* [PR 354](https://github.com/spyder-ide/qtpy/pull/354) - PR: Add wrapper around sip/shiboken isdeleted/isvalid (compat.py), by [@zjp](https://github.com/zjp) ([302](https://github.com/spyder-ide/qtpy/issues/302)) +* [PR 353](https://github.com/spyder-ide/qtpy/pull/353) - PR: Add note to readme about use with Pyright, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([352](https://github.com/spyder-ide/qtpy/issues/352)) +* [PR 350](https://github.com/spyder-ide/qtpy/pull/350) - PR: Restore `WEBENGINE` constant in `QtWebEngineWidgets`, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 346](https://github.com/spyder-ide/qtpy/pull/346) - PR: Add workaround for `mode` argument in QTextCursor.movePosition (PySide6), by [@rear1019](https://github.com/rear1019) +* [PR 344](https://github.com/spyder-ide/qtpy/pull/344) - PR: Add missing imports and modules, by [@DaelonSuzuka](https://github.com/DaelonSuzuka) + +In this release 7 pull requests were closed. + + +---- + + +## Version 2.1.0 (2022-05-02) + +### New features + +* New CLI to get mypy arguments and check QtPy version + +### Important fixes + +* Remove Python 3.6 support +* Fix `QT_API` environmental variable handling so new processes get the correct value when `Qt_API` is not initially set +* Change try order for bindings in case the `QT_API` environmental variable is not set i.e `PyQt5 - PySide2 - PyQt6 - PySide6` + +### Issues Closed + +* [Issue 342](https://github.com/spyder-ide/qtpy/issues/342) - Change bindings `try order` to follow QtPy `v1.x` convention ([PR 343](https://github.com/spyder-ide/qtpy/pull/343) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 338](https://github.com/spyder-ide/qtpy/issues/338) - Release QtPy 2.1.0 +* [Issue 336](https://github.com/spyder-ide/qtpy/issues/336) - Segfaulting tests on PyQt6 + Windows on both Py3.7 and 3.10 (no conda) probably due to PyQt 6.3.0 ([PR 335](https://github.com/spyder-ide/qtpy/pull/335) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 332](https://github.com/spyder-ide/qtpy/issues/332) - Remove `QHeaderView` patch ([PR 334](https://github.com/spyder-ide/qtpy/pull/334) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 327](https://github.com/spyder-ide/qtpy/issues/327) - Missing `QtCore.Qt.MidButton` alias for `QtCore.Qt.MouseButton.MiddleButton` with PyQt6 ([PR 328](https://github.com/spyder-ide/qtpy/pull/328) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 323](https://github.com/spyder-ide/qtpy/issues/323) - Fix test suite failing at collection on Python 3.10 in Pytest 7.x ([PR 324](https://github.com/spyder-ide/qtpy/pull/324) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 297](https://github.com/spyder-ide/qtpy/issues/297) - Drop Python 3.6 support ([PR 329](https://github.com/spyder-ide/qtpy/pull/329) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 265](https://github.com/spyder-ide/qtpy/issues/265) - Investigate and (hopefully) resolve segfaults and other issues in UIC tests ([PR 335](https://github.com/spyder-ide/qtpy/pull/335) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 216](https://github.com/spyder-ide/qtpy/issues/216) - Support type hints and mypy ([PR 337](https://github.com/spyder-ide/qtpy/pull/337) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) + +In this release 9 issues were closed. + +### Pull Requests Merged + +* [PR 343](https://github.com/spyder-ide/qtpy/pull/343) - PR: Change bindings `try order` to be `PyQt5, PySide2, PyQt6, PySide6`, by [@dalthviz](https://github.com/dalthviz) ([342](https://github.com/spyder-ide/qtpy/issues/342)) +* [PR 341](https://github.com/spyder-ide/qtpy/pull/341) - PR: Add workaround for `mode` argument in QTextCursor.movePosition (Pyside2), by [@rear1019](https://github.com/rear1019) +* [PR 340](https://github.com/spyder-ide/qtpy/pull/340) - PR: Fix bug with environ handling, by [@larsoner](https://github.com/larsoner) +* [PR 337](https://github.com/spyder-ide/qtpy/pull/337) - PR: Add command line support for Mypy, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([217](https://github.com/spyder-ide/qtpy/issues/217), [216](https://github.com/spyder-ide/qtpy/issues/216)) +* [PR 335](https://github.com/spyder-ide/qtpy/pull/335) - PR: Fix uic skipped tests and PyQt 6.3.0 segfaulting tests, by [@dalthviz](https://github.com/dalthviz) ([336](https://github.com/spyder-ide/qtpy/issues/336), [265](https://github.com/spyder-ide/qtpy/issues/265)) +* [PR 334](https://github.com/spyder-ide/qtpy/pull/334) - PR: Remove `QHeaderView` patch related files, by [@dalthviz](https://github.com/dalthviz) ([332](https://github.com/spyder-ide/qtpy/issues/332)) +* [PR 333](https://github.com/spyder-ide/qtpy/pull/333) - PR: Skip import of QOpenGLTime* on architectures where not available, by [@juliangilbey](https://github.com/juliangilbey) +* [PR 331](https://github.com/spyder-ide/qtpy/pull/331) - PR: Use QFileDialog.Option to be compatiable with Qt6, by [@frmdstryr](https://github.com/frmdstryr) +* [PR 330](https://github.com/spyder-ide/qtpy/pull/330) - PR: Fix typo in constant name, by [@eyllanesc](https://github.com/eyllanesc) +* [PR 329](https://github.com/spyder-ide/qtpy/pull/329) - PR: Drop support for Python 3.6, by [@dalthviz](https://github.com/dalthviz) ([297](https://github.com/spyder-ide/qtpy/issues/297)) +* [PR 328](https://github.com/spyder-ide/qtpy/pull/328) - PR: Add missing `Qt.MidButton` on PyQt6, by [@dalthviz](https://github.com/dalthviz) ([327](https://github.com/spyder-ide/qtpy/issues/327)) +* [PR 326](https://github.com/spyder-ide/qtpy/pull/326) - PR: Add missing `QWebEngineScript` support for PyQt5/6 and PySide2/6, by [@EasyIsrael](https://github.com/EasyIsrael) +* [PR 325](https://github.com/spyder-ide/qtpy/pull/325) - PR: Monkey patch `pyside2uic` `UIParser.readResources` for Python 3.9 compatibility, by [@n-elie](https://github.com/n-elie) +* [PR 324](https://github.com/spyder-ide/qtpy/pull/324) - PR: Restrict broken Pytest versions to those not affected by the Pytest 7.0.0 import-mode=importlib behavior regression, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([323](https://github.com/spyder-ide/qtpy/issues/323)) + +In this release 14 pull requests were closed. + + +---- + + +## Version 2.0.1 (2022-02-02) + +### Issues Closed + +* [Issue 320](https://github.com/spyder-ide/qtpy/issues/320) - Release QtPy 2.0.1 +* [Issue 316](https://github.com/spyder-ide/qtpy/issues/316) - Tests for instance methods alias mapping fix (exec_ vs exec and others) ([PR 317](https://github.com/spyder-ide/qtpy/pull/317) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 311](https://github.com/spyder-ide/qtpy/issues/311) - `QtCore.Qt.mightBeRichText` undefined in PySide ([PR 313](https://github.com/spyder-ide/qtpy/pull/313) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 306](https://github.com/spyder-ide/qtpy/issues/306) - Qt6: missing unscoped enum values ([PR 314](https://github.com/spyder-ide/qtpy/pull/314) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 305](https://github.com/spyder-ide/qtpy/issues/305) - Qt6: exec_ vs. exec ([PR 308](https://github.com/spyder-ide/qtpy/pull/308) by [@kumattau](https://github.com/kumattau)) +* [Issue 304](https://github.com/spyder-ide/qtpy/issues/304) - Qt6: QtCharts namespace incompatibility ([PR 315](https://github.com/spyder-ide/qtpy/pull/315) by [@dalthviz](https://github.com/dalthviz)) + +In this release 6 issues were closed. + +### Pull Requests Merged + +* [PR 319](https://github.com/spyder-ide/qtpy/pull/319) - PR: Promote enum aliases, by [@MatthieuDartiailh](https://github.com/MatthieuDartiailh) +* [PR 317](https://github.com/spyder-ide/qtpy/pull/317) - PR: Add missing tests for aliased methods, by [@dalthviz](https://github.com/dalthviz) ([316](https://github.com/spyder-ide/qtpy/issues/316)) +* [PR 315](https://github.com/spyder-ide/qtpy/pull/315) - PR: Add `QtCharts` alias for backward compatibility with 1.x, by [@dalthviz](https://github.com/dalthviz) ([304](https://github.com/spyder-ide/qtpy/issues/304)) +* [PR 314](https://github.com/spyder-ide/qtpy/pull/314) - PR: Add mapping for missing enum values aliases on `QtCore.Qt`, by [@dalthviz](https://github.com/dalthviz) ([306](https://github.com/spyder-ide/qtpy/issues/306)) +* [PR 313](https://github.com/spyder-ide/qtpy/pull/313) - PR: Add missing `QtGui` utility function to `QtCore.Qt` for PySide bindings, by [@dalthviz](https://github.com/dalthviz) ([311](https://github.com/spyder-ide/qtpy/issues/311)) +* [PR 312](https://github.com/spyder-ide/qtpy/pull/312) - PR: Add "New features" section for version 2.0 to changelog, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 309](https://github.com/spyder-ide/qtpy/pull/309) - PR: Set CI job's timeout to 10 min to force a stalled test to terminate, by [@kumattau](https://github.com/kumattau) +* [PR 308](https://github.com/spyder-ide/qtpy/pull/308) - PR: Fix mappings of instance method and slot alias (PyQt6 and PySide6), by [@kumattau](https://github.com/kumattau) ([305](https://github.com/spyder-ide/qtpy/issues/305)) +* [PR 307](https://github.com/spyder-ide/qtpy/pull/307) - PR: Add missing imports in QtOpenGL, by [@renefritze](https://github.com/renefritze) + +In this release 9 pull requests were closed. + + +---- + + + +## Version 2.0.0 (2021-12-22) + +### New features + +* Add support for PyQt6 and PySide6 with Qt 6 >= 6.2.0 +* Add support for QtTextToSpeech. +* Drop support for PyQt4, PySide and Python 2. + +### Issues Closed + +* [Issue 300](https://github.com/spyder-ide/qtpy/issues/300) - Release QtPy 2.0.0 +* [Issue 286](https://github.com/spyder-ide/qtpy/issues/286) - PySide6 DeprecationWarning of exec_ ([PR 287](https://github.com/spyder-ide/qtpy/pull/287) by [@kumattau](https://github.com/kumattau)) +* [Issue 274](https://github.com/spyder-ide/qtpy/issues/274) - SignalInstance, Slot, and Property for Qt6 QtCore +* [Issue 270](https://github.com/spyder-ide/qtpy/issues/270) - Port packaging from deprecated legacy builder to current PEP 517 standards ([PR 272](https://github.com/spyder-ide/qtpy/pull/272) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 269](https://github.com/spyder-ide/qtpy/issues/269) - Add CI testing and official support for Python 3.10 ([PR 296](https://github.com/spyder-ide/qtpy/pull/296) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 267](https://github.com/spyder-ide/qtpy/issues/267) - Coveralls shows 0 coverage despite coverage report and runner output looking fine ([PR 268](https://github.com/spyder-ide/qtpy/pull/268) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 258](https://github.com/spyder-ide/qtpy/issues/258) - Compatibility with `QtWidgets.QOpenGLWidget` vs `QtOpenGLWidgets.QOpenGLWidget` ([PR 259](https://github.com/spyder-ide/qtpy/pull/259) by [@kumattau](https://github.com/kumattau)) +* [Issue 257](https://github.com/spyder-ide/qtpy/issues/257) - Check import behavior for QtCharts module ([PR 260](https://github.com/spyder-ide/qtpy/pull/260) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 253](https://github.com/spyder-ide/qtpy/issues/253) - Declare and test support up to Python 3.9 and PyQt6 ([PR 262](https://github.com/spyder-ide/qtpy/pull/262) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 250](https://github.com/spyder-ide/qtpy/issues/250) - Drop Python 2 support ([PR 251](https://github.com/spyder-ide/qtpy/pull/251) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 234](https://github.com/spyder-ide/qtpy/issues/234) - distutils.LooseVersion is being deprecated ([PR 266](https://github.com/spyder-ide/qtpy/pull/266) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 233](https://github.com/spyder-ide/qtpy/issues/233) - PyQt6 support ([PR 294](https://github.com/spyder-ide/qtpy/pull/294) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 232](https://github.com/spyder-ide/qtpy/issues/232) - Add support for `QtTextToSpeech` +* [Issue 197](https://github.com/spyder-ide/qtpy/issues/197) - QtTest imports are incomplete ([PR 290](https://github.com/spyder-ide/qtpy/pull/290) by [@kumattau](https://github.com/kumattau)) +* [Issue 70](https://github.com/spyder-ide/qtpy/issues/70) - Add QHeaderView.setSectionResize for Qt4 layer +* [Issue 66](https://github.com/spyder-ide/qtpy/issues/66) - Add documentation for how to run tests ([PR 277](https://github.com/spyder-ide/qtpy/pull/277) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 62](https://github.com/spyder-ide/qtpy/issues/62) - Add to the readme the importance of qtpy when migrating an application ([PR 301](https://github.com/spyder-ide/qtpy/pull/301) by [@dalthviz](https://github.com/dalthviz)) + +In this release 17 issues were closed. + +### Pull Requests Merged + +* [PR 303](https://github.com/spyder-ide/qtpy/pull/303) - PR: Fix QFileSystemModel for PyQt6, by [@almarklein](https://github.com/almarklein) +* [PR 301](https://github.com/spyder-ide/qtpy/pull/301) - PR: Update README.md adding a line for QtPy usefulness when migrating between Qt bindings/versions, by [@dalthviz](https://github.com/dalthviz) ([62](https://github.com/spyder-ide/qtpy/issues/62)) +* [PR 299](https://github.com/spyder-ide/qtpy/pull/299) - PR: 'width' needed for PyQt6, by [@stonebig](https://github.com/stonebig) +* [PR 298](https://github.com/spyder-ide/qtpy/pull/298) - PR: Make QLibraryInfo.location work for PyQt6, by [@stonebig](https://github.com/stonebig) +* [PR 296](https://github.com/spyder-ide/qtpy/pull/296) - PR: Add support for Python 3.10 on CIs and packaging, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([269](https://github.com/spyder-ide/qtpy/issues/269)) +* [PR 294](https://github.com/spyder-ide/qtpy/pull/294) - PR: Add minimum version check for Qt6 >=6.2.0, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([233](https://github.com/spyder-ide/qtpy/issues/233)) +* [PR 292](https://github.com/spyder-ide/qtpy/pull/292) - PR: Define Qt/binding versions at top level, fix warnings if versions not found, and fix test dir on CIs, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 291](https://github.com/spyder-ide/qtpy/pull/291) - PR: Add QtTextToSpeech Module, by [@kumattau](https://github.com/kumattau) +* [PR 290](https://github.com/spyder-ide/qtpy/pull/290) - PR: Import all classes in QtTest module., by [@kumattau](https://github.com/kumattau) ([197](https://github.com/spyder-ide/qtpy/issues/197)) +* [PR 287](https://github.com/spyder-ide/qtpy/pull/287) - PR: Map exec_ to their non-deprecated alternatives, by [@kumattau](https://github.com/kumattau) ([286](https://github.com/spyder-ide/qtpy/issues/286)) +* [PR 282](https://github.com/spyder-ide/qtpy/pull/282) - PR: Update README sponsors section, by [@dalthviz](https://github.com/dalthviz) +* [PR 279](https://github.com/spyder-ide/qtpy/pull/279) - PR: Add back legacy PYQT4 and PYSIDE package-level constants for compat, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 278](https://github.com/spyder-ide/qtpy/pull/278) - PR: Update gitignore, gitattributes, license, authors and security policy, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 277](https://github.com/spyder-ide/qtpy/pull/277) - PR: Add Contributing Guide based on other Spyder projects, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([66](https://github.com/spyder-ide/qtpy/issues/66)) +* [PR 276](https://github.com/spyder-ide/qtpy/pull/276) - PR: Fix regression in FORCE_QT_API behavior from merging PySide6 support, by [@tlambert03](https://github.com/tlambert03) +* [PR 273](https://github.com/spyder-ide/qtpy/pull/273) - PR: Avoid future deprecations and decrease general technical debt, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) +* [PR 272](https://github.com/spyder-ide/qtpy/pull/272) - PR: Upgrade packaging to avoid legacy/deprecated behavior and follow PEP 517, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([270](https://github.com/spyder-ide/qtpy/issues/270)) +* [PR 271](https://github.com/spyder-ide/qtpy/pull/271) - PR: Unscoped enums access for PyQt6 and other missing PyQt6 compatibility changes, by [@dalthviz](https://github.com/dalthviz) +* [PR 268](https://github.com/spyder-ide/qtpy/pull/268) - PR: Fix and improve Coveralls reporting, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([267](https://github.com/spyder-ide/qtpy/issues/267)) +* [PR 266](https://github.com/spyder-ide/qtpy/pull/266) - PR: Use modern packaging instead of deprecated distutils for version parse, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([235](https://github.com/spyder-ide/qtpy/issues/235), [234](https://github.com/spyder-ide/qtpy/issues/234)) +* [PR 264](https://github.com/spyder-ide/qtpy/pull/264) - PR: [PyQt6] Add missing tabStopWidth/setTabStopWidth same as PySide6, by [@kumattau](https://github.com/kumattau) +* [PR 263](https://github.com/spyder-ide/qtpy/pull/263) - PR: Remove accidentally-duplicated lines of code in QtCore, by [@kumattau](https://github.com/kumattau) +* [PR 262](https://github.com/spyder-ide/qtpy/pull/262) - PR: Unify and cleanup CI infra, improve robustness and test Python 3.9 and PyQt6, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([253](https://github.com/spyder-ide/qtpy/issues/253)) +* [PR 260](https://github.com/spyder-ide/qtpy/pull/260) - PR: Reorganize `QtCharts` module import and add missing skip validation for `QtNetworkAuth` test with `PyQt6`, by [@dalthviz](https://github.com/dalthviz) ([257](https://github.com/spyder-ide/qtpy/issues/257)) +* [PR 259](https://github.com/spyder-ide/qtpy/pull/259) - PR: [PyQt6] Add exec_/print_, and add QOpenGLWidget/QShortcut to QtWidgets, by [@kumattau](https://github.com/kumattau) ([258](https://github.com/spyder-ide/qtpy/issues/258)) +* [PR 256](https://github.com/spyder-ide/qtpy/pull/256) - PR: Fix fallback API, by [@benoit-pierre](https://github.com/benoit-pierre) +* [PR 255](https://github.com/spyder-ide/qtpy/pull/255) - PR: Add additional Qt modules and more support and tests for PyQt6/PySide6, by [@jschueller](https://github.com/jschueller) +* [PR 254](https://github.com/spyder-ide/qtpy/pull/254) - PR: Add QShortcut class to QtWidgets module, by [@kumattau](https://github.com/kumattau) +* [PR 252](https://github.com/spyder-ide/qtpy/pull/252) - PR: Remove Qt4 support, by [@jschueller](https://github.com/jschueller) +* [PR 251](https://github.com/spyder-ide/qtpy/pull/251) - PR: Drop Python 2, by [@dalthviz](https://github.com/dalthviz) ([250](https://github.com/spyder-ide/qtpy/issues/250)) +* [PR 225](https://github.com/spyder-ide/qtpy/pull/225) - PR: Add support for PySide6, by [@jschueller](https://github.com/jschueller) + +In this release 31 pull requests were closed. + + +---- + + +## Version 1.11.3 (2021/12/03) + +### Issues Closed + +* [Issue 284](https://github.com/spyder-ide/qtpy/issues/284) - Warn if using a deprecated and/or unsupported Qt 5 version ([PR 289](https://github.com/spyder-ide/qtpy/pull/289) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) +* [Issue 280](https://github.com/spyder-ide/qtpy/issues/280) - import qtpy raise except when only have PyQt4 ([PR 281](https://github.com/spyder-ide/qtpy/pull/281) by [@dalthviz](https://github.com/dalthviz)) +* [Issue 261](https://github.com/spyder-ide/qtpy/issues/261) - Add a deprecation warning for unsupported `Qt` versions and bindings (at least `Qt4`: `PyQt4` and `PySide`) ([PR 283](https://github.com/spyder-ide/qtpy/pull/283) by [@CAM-Gerlach](https://github.com/CAM-Gerlach)) + +In this release 3 issues were closed. + +### Pull Requests Merged + +* [PR 293](https://github.com/spyder-ide/qtpy/pull/293) - PR: Update RELEASE.md for 1.x version, by [@dalthviz](https://github.com/dalthviz) +* [PR 289](https://github.com/spyder-ide/qtpy/pull/289) - PR: Add warning for deprecated/EoL Qt5 & PyQt5/PySide2 versions to fix #284, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([284](https://github.com/spyder-ide/qtpy/issues/284)) +* [PR 285](https://github.com/spyder-ide/qtpy/pull/285) - PR: Import Callable and MutableMapping in py3compat for Python 2 compat, by [@davvid](https://github.com/davvid) +* [PR 283](https://github.com/spyder-ide/qtpy/pull/283) - PR: Add a warning for developers still running legacy Qt4-based APIs, by [@CAM-Gerlach](https://github.com/CAM-Gerlach) ([261](https://github.com/spyder-ide/qtpy/issues/261)) +* [PR 281](https://github.com/spyder-ide/qtpy/pull/281) - PR: Catch `PythonQtError` when trying to do alias for `QtDataVisualization` and dev version correction, by [@dalthviz](https://github.com/dalthviz) ([280](https://github.com/spyder-ide/qtpy/issues/280)) + +In this release 5 pull requests were closed. + + +---- + + +## Version 1.11.2 (2021-09-23) + +### Issues Closed + +* [Issue 248](https://github.com/spyder-ide/qtpy/issues/248) - Missing QtDataVisualization ([PR 249](https://github.com/spyder-ide/qtpy/pull/249) by [@dalthviz](https://github.com/dalthviz)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 249](https://github.com/spyder-ide/qtpy/pull/249) - PR: Add handling for QtDataVisualization when missing, by [@dalthviz](https://github.com/dalthviz) ([248](https://github.com/spyder-ide/qtpy/issues/248)) + +In this release 1 pull request was closed. + + +---- + + +## Version 1.11.1 (2021-09-13) + +### Issues Closed + +* [Issue 245](https://github.com/spyder-ide/qtpy/issues/245) - Importing `qtpy.uic` raises an exception ([PR 246](https://github.com/spyder-ide/qtpy/pull/246) by [@dalthviz](https://github.com/dalthviz)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 246](https://github.com/spyder-ide/qtpy/pull/246) - PR: Wrap `pysideuic` and `pyside2uic` imports since they could be unavailable, by [@dalthviz](https://github.com/dalthviz) ([245](https://github.com/spyder-ide/qtpy/issues/245)) +* [PR 244](https://github.com/spyder-ide/qtpy/pull/244) - qtpy/tests/test_uic.py: skip if pyside2uic not installed, by [@AndrewAmmerlaan](https://github.com/AndrewAmmerlaan) + +In this release 2 pull requests were closed. + + +---- + + +## Version 1.11.0 (2021-09-03) + +### Issues Closed + +* [Issue 201](https://github.com/spyder-ide/qtpy/issues/201) - Missing QWebEngineProfile from QtWebEngineWidgets ([PR 242](https://github.com/spyder-ide/qtpy/pull/242) by [@dalthviz](https://github.com/dalthviz)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 243](https://github.com/spyder-ide/qtpy/pull/243) - PR: `QtDataVisualization` backward compatibility handling on Windows, by [@dalthviz](https://github.com/dalthviz) +* [PR 242](https://github.com/spyder-ide/qtpy/pull/242) - PR: Add `QtWebEngineWidgets.QWebEngineProfile` for PyQt5 and PySide2, by [@dalthviz](https://github.com/dalthviz) ([201](https://github.com/spyder-ide/qtpy/issues/201)) +* [PR 228](https://github.com/spyder-ide/qtpy/pull/228) - PR: Rename QtDatavisualization to use uppercase v, by [@antlarr](https://github.com/antlarr) +* [PR 219](https://github.com/spyder-ide/qtpy/pull/219) - PR: Add support for QStyleOptionFrameV3 from PyQt4, by [@PierreRaybaut](https://github.com/PierreRaybaut) +* [PR 218](https://github.com/spyder-ide/qtpy/pull/218) - PR: Add QtWinExtras module, by [@phil65](https://github.com/phil65) +* [PR 209](https://github.com/spyder-ide/qtpy/pull/209) - PR: Add support for QtSerialPort add-on, by [@Stanowczo](https://github.com/Stanowczo) +* [PR 205](https://github.com/spyder-ide/qtpy/pull/205) - PR: Add support for the QtPositioning module, by [@avalentino](https://github.com/avalentino) +* [PR 202](https://github.com/spyder-ide/qtpy/pull/202) - PR: Add loadUiType implementation for PySide2, by [@avalentino](https://github.com/avalentino) + +In this release 8 pull requests were closed. + + +---- + + +## Version 1.10.0 (2021-08-17) + +### Issues Closed + +* [Issue 238](https://github.com/spyder-ide/qtpy/issues/238) - PySide2 and Python3.9: xml.etree.ElementTree.Element' object has no attribute 'getchildren +* [Issue 222](https://github.com/spyder-ide/qtpy/issues/222) - Imported modules are not respected +* [Issue 220](https://github.com/spyder-ide/qtpy/issues/220) - MNT: Stop using ci-helpers in appveyor.yml +* [Issue 206](https://github.com/spyder-ide/qtpy/issues/206) - DeprecationWarning for getchildren ([PR 224](https://github.com/spyder-ide/qtpy/pull/224) by [@irrcombat](https://github.com/irrcombat)) +* [Issue 198](https://github.com/spyder-ide/qtpy/issues/198) - PyQt4-sip==4.19.13 not supported + +In this release 5 issues were closed. + +### Pull Requests Merged + +* [PR 241](https://github.com/spyder-ide/qtpy/pull/241) - PR: Update setup.py classifiers, by [@dalthviz](https://github.com/dalthviz) +* [PR 230](https://github.com/spyder-ide/qtpy/pull/230) - PR: Fix imported modules logic if 'FORCE_QT_API' is empty, by [@hiaselhans](https://github.com/hiaselhans) +* [PR 224](https://github.com/spyder-ide/qtpy/pull/224) - PR: Support python 3.9 `custom_widgets` iteration, by [@irrcombat](https://github.com/irrcombat) ([206](https://github.com/spyder-ide/qtpy/issues/206)) +* [PR 215](https://github.com/spyder-ide/qtpy/pull/215) - PR: Slight typo fix, by [@altendky](https://github.com/altendky) +* [PR 214](https://github.com/spyder-ide/qtpy/pull/214) - PR: Handle QtCore.SignalInstance/pyqtBoundSignal, by [@altendky](https://github.com/altendky) +* [PR 208](https://github.com/spyder-ide/qtpy/pull/208) - PR: Move CI to Github Actions, by [@goanpeca](https://github.com/goanpeca) +* [PR 204](https://github.com/spyder-ide/qtpy/pull/204) - PR: Add Python 3.9 compatibility for `collections.abc` module, by [@tirkarthi](https://github.com/tirkarthi) +* [PR 199](https://github.com/spyder-ide/qtpy/pull/199) - PR: Add support to PyQt4-sip 4.19.13, by [@milanmatic](https://github.com/milanmatic) + +In this release 8 pull requests were closed. + + +---- + + +## Version 1.9.0 (2019-07-23) + +### New features + +* Add the FORCE_QT_API environment variable to keep using the Qt + bindings selected with the QT_API variable and avoid switching + to the currently imported bindings. This allows to have + applications that import PySide and PyQt bindings at the same + time (which is possible if both bindings are compiled for the + same Qt version). + +### Issues Closed + +* [Issue 195](https://github.com/spyder-ide/qtpy/issues/195) - Errors in the Qt3D modules with PySide2 5.12.4+ and Python 2 ([PR 196](https://github.com/spyder-ide/qtpy/pull/196)) +* [Issue 192](https://github.com/spyder-ide/qtpy/issues/192) - Binding Selection Logic ([PR 194](https://github.com/spyder-ide/qtpy/pull/194)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 196](https://github.com/spyder-ide/qtpy/pull/196) - PR: Don't load Qt3D modules for buggy versions of PySide2 ([195](https://github.com/spyder-ide/qtpy/issues/195)) +* [PR 194](https://github.com/spyder-ide/qtpy/pull/194) - PR: Add FORCE_QT_API environment variable ([192](https://github.com/spyder-ide/qtpy/issues/192)) + +In this release 2 pull requests were closed. + + +---- + + +## Version 1.8.0 (2019-06-12) + +### New features + +* Add support for several Qt 3D modules. + +### Issues Closed + +* [Issue 172](https://github.com/spyder-ide/qtpy/issues/172) - Support for Qt3D ([PR 191](https://github.com/spyder-ide/qtpy/pull/191)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 191](https://github.com/spyder-ide/qtpy/pull/191) - PR: Add Qt 3D bindings ([172](https://github.com/spyder-ide/qtpy/issues/172)) + +In this release 1 pull request was closed. + + +---- + + +## Version 1.7.1 (2019-05-05) + + +### Pull Requests Merged + +* [PR 189](https://github.com/spyder-ide/qtpy/pull/189) - PR: Skip testing PyQt4 and PySide in Python 3.5 +* [PR 188](https://github.com/spyder-ide/qtpy/pull/188) - PR: Trivial maintenance tweaks +* [PR 187](https://github.com/spyder-ide/qtpy/pull/187) - PR: Avoid deprecated "from collections import MutableMapping" + +In this release 3 pull requests were closed. + + +---- + + +## Version 1.7.0 (2019-03-16) + +### New features + +* Add support for QtCharts. + +### Pull Requests Merged + +* [PR 186](https://github.com/spyder-ide/qtpy/pull/186) - PR: Generate PyPI long description from README.md +* [PR 183](https://github.com/spyder-ide/qtpy/pull/183) - PR: Add QtCharts module support +* [PR 182](https://github.com/spyder-ide/qtpy/pull/182) - PR: Prevent warnings for equivalent APIs +* [PR 176](https://github.com/spyder-ide/qtpy/pull/176) - PR: Don't warn about bindings change if user did not specify a binding + +In this release 4 pull requests were closed. + + +---- + + +## Version 1.6 (2019-01-12) + +### New features + +* Add support for QtQuickWidgets. + +### Issues Closed + +* [Issue 178](https://github.com/spyder-ide/qtpy/issues/178) - Error when import QtCore.__version__ in PySide2 ([PR 180](https://github.com/spyder-ide/qtpy/pull/180)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 181](https://github.com/spyder-ide/qtpy/pull/181) - PR: Restore QWebEngineSettings for PySide2 +* [PR 180](https://github.com/spyder-ide/qtpy/pull/180) - PR: Add QtCore.__version__ for PySide2 ([178](https://github.com/spyder-ide/qtpy/issues/178)) +* [PR 179](https://github.com/spyder-ide/qtpy/pull/179) - PR: Add QtQuickWidgets + +In this release 3 pull requests were closed. + + +---- + + +## Version 1.5.2 (2018-10-20) + + +### Pull Requests Merged + +* [PR 175](https://github.com/spyder-ide/qtpy/pull/175) - PR: Fix tests +* [PR 174](https://github.com/spyder-ide/qtpy/pull/174) - PR: Add support for PySide2.QtOpenGL + +In this release 2 pull requests were closed. + + +---- + + +## Version 1.5.1 (2018-09-18) + +### Issues Closed + +* [Issue 170](https://github.com/spyder-ide/qtpy/issues/170) - Can't catch PythonQtError ([PR 173](https://github.com/spyder-ide/qtpy/pull/173)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 173](https://github.com/spyder-ide/qtpy/pull/173) - PR: Make PythonQtError inherit from RuntimeError to be easily catchable ([170](https://github.com/spyder-ide/qtpy/issues/170)) + +In this release 1 pull request was closed. + +---- + + +## Version 1.5 (2018-08-25) + +### New features + +* Add support for QtLocation, QtMultimediaWidgets, QtQml, QtQuick, + QtWebChannel, QtWebSockets and QtXmlPatterns. +* Raise an error when trying to use the wrong combination of macOS + and Qt versions. + +### Issues Closed + +* [Issue 155](https://github.com/spyder-ide/qtpy/issues/155) - Add warnings for Qt 5.9 in macOS 10.9 and Qt 5.11 and macOS 10.11 ([PR 168](https://github.com/spyder-ide/qtpy/pull/168)) +* [Issue 153](https://github.com/spyder-ide/qtpy/issues/153) - Shim PyQt5 ToPyDateTime for compatibility with PySide2 ([PR 169](https://github.com/spyder-ide/qtpy/pull/169)) +* [Issue 123](https://github.com/spyder-ide/qtpy/issues/123) - Wrap QWebChannel module ([PR 157](https://github.com/spyder-ide/qtpy/pull/157)) + +In this release 3 issues were closed. + +### Pull Requests Merged + +* [PR 169](https://github.com/spyder-ide/qtpy/pull/169) - PR: Shim PyQt5 QDateTime.toPyDateTime to QDateTime.toPython for compatibility with PySide2 ([153](https://github.com/spyder-ide/qtpy/issues/153)) +* [PR 168](https://github.com/spyder-ide/qtpy/pull/168) - PR: Raise error when trying to use the wrong combination of macOS and Qt versions ([155](https://github.com/spyder-ide/qtpy/issues/155)) +* [PR 167](https://github.com/spyder-ide/qtpy/pull/167) - PR: Migrate to CircleCI 2.0 +* [PR 163](https://github.com/spyder-ide/qtpy/pull/163) - PR: Add QtLocation +* [PR 162](https://github.com/spyder-ide/qtpy/pull/162) - PR: Update readme to remove funding appeal, harmonize with other readmes and minor fixes +* [PR 161](https://github.com/spyder-ide/qtpy/pull/161) - PR: Fix pyside2 wheels install +* [PR 157](https://github.com/spyder-ide/qtpy/pull/157) - PR: Add more Qt modules ([123](https://github.com/spyder-ide/qtpy/issues/123)) + +In this release 7 pull requests were closed. + + +---- + + +## Version 1.4.2 (2018-05-06) + +### Issues Closed + +* [Issue 150](https://github.com/spyder-ide/qtpy/issues/150) - PySide2-5.11 alpha2 compatibility ([PR 151](https://github.com/spyder-ide/qtpy/pull/151)) +* [Issue 144](https://github.com/spyder-ide/qtpy/issues/144) - ValueError: API 'QString' has already been set to version 1 at line 141 in __init__.py file. ([PR 152](https://github.com/spyder-ide/qtpy/pull/152)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 152](https://github.com/spyder-ide/qtpy/pull/152) - PR: Catch ValueError when trying to set sip API ([144](https://github.com/spyder-ide/qtpy/issues/144)) +* [PR 151](https://github.com/spyder-ide/qtpy/pull/151) - PR: Add a preventive change for PySide-5.11a2 ([150](https://github.com/spyder-ide/qtpy/issues/150)) +* [PR 149](https://github.com/spyder-ide/qtpy/pull/149) - PR: Use Qt official wheels to run tests for PySide2 +* [PR 148](https://github.com/spyder-ide/qtpy/pull/148) - PR: Remove internal conda recipe + +In this release 4 pull requests were closed. + + +---- + + +## Version 1.4.1 (2018-04-28) + +### New features + +* Show a warning when QT_API is changed automatically by qtpy. + +### Issues Closed + +* [Issue 145](https://github.com/spyder-ide/qtpy/issues/145) - Raise a warning if QT_API value is changed automatically ([PR 146](https://github.com/spyder-ide/qtpy/pull/146)) +* [Issue 142](https://github.com/spyder-ide/qtpy/issues/142) - On OSX qtpy applications are forcing discrete graphics ([PR 143](https://github.com/spyder-ide/qtpy/pull/143)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 147](https://github.com/spyder-ide/qtpy/pull/147) - PR: Add better compatibility with PySide2 +* [PR 146](https://github.com/spyder-ide/qtpy/pull/146) - PR: Add a warning if API is changed automatically ([145](https://github.com/spyder-ide/qtpy/issues/145)) +* [PR 143](https://github.com/spyder-ide/qtpy/pull/143) - PR: Avoid using PyQt5.Qt, which imports unneeded stuff and forces discrete GPU on OSX ([142](https://github.com/spyder-ide/qtpy/issues/142)) + +In this release 3 pull requests were closed. + + +---- + + +## Version 1.4 (2018-03-11) + +### New features + +* Add support for QtHelp and QtSql +* Use already imported bindings + +### Issues Closed + +* [Issue 138](https://github.com/spyder-ide/qtpy/issues/138) - If one binding has already been imported, then qtpy should just use it ([PR 139](https://github.com/spyder-ide/qtpy/pull/139)) +* [Issue 135](https://github.com/spyder-ide/qtpy/issues/135) - Add Wrapper for QtSql [feature request] ([PR 136](https://github.com/spyder-ide/qtpy/pull/136)) +* [Issue 131](https://github.com/spyder-ide/qtpy/issues/131) - Methods missing from QStandardPaths when QT_API=pyqt4 +* [Issue 127](https://github.com/spyder-ide/qtpy/issues/127) - Add Wrapper for QtHelp [feature request] ([PR 128](https://github.com/spyder-ide/qtpy/pull/128)) + +In this release 4 issues were closed. + +### Pull Requests Merged + +* [PR 140](https://github.com/spyder-ide/qtpy/pull/140) - PR: Pin PyQt5 to 5.9.2 in CircleCI because 5.10 is generating segfaults +* [PR 139](https://github.com/spyder-ide/qtpy/pull/139) - PR: If a Qt binding is already imported, then use it. ([138](https://github.com/spyder-ide/qtpy/issues/138)) +* [PR 136](https://github.com/spyder-ide/qtpy/pull/136) - PR: Add QtSql wrapper (incl. test) ([135](https://github.com/spyder-ide/qtpy/issues/135)) +* [PR 132](https://github.com/spyder-ide/qtpy/pull/132) - PR: Changes to QDesktop split +* [PR 128](https://github.com/spyder-ide/qtpy/pull/128) - PR: Add QtHelp Wrapper ([127](https://github.com/spyder-ide/qtpy/issues/127)) + +In this release 5 pull requests were closed. + + +---- + + +## Version 1.3.1 (2017-08-21) + +### Bugs fixed + +**Issues** + +* [Issue 129](https://github.com/spyder-ide/qtpy/issues/129) - Spurious cache files in PyPI tarball +* [Issue 119](https://github.com/spyder-ide/qtpy/issues/119) - Importing qtpy should not raise exceptions + +In this release 2 issues were closed + +**Pull requests** + +* [PR 130](https://github.com/spyder-ide/qtpy/pull/130) - PR: No cache files included in the release tarball +* [PR 126](https://github.com/spyder-ide/qtpy/pull/126) - PR: Remove Quantified Code badge because the service doesn't exist anymore +* [PR 121](https://github.com/spyder-ide/qtpy/pull/121) - PR: Warn if QHeaderView deprecated methods are used + +In this release 3 pull requests were merged + + +---- + + +## Version 1.3 (2017-08-12) + +### New features + +* Add support for PySide2 and PyQt 4.6 + +### Bugs fixed + +**Issues** + +* [Issue 124](https://github.com/spyder-ide/qtpy/issues/124) - Typo in readme title +* [Issue 111](https://github.com/spyder-ide/qtpy/issues/111) - Update Readme for 1.3 release +* [Issue 110](https://github.com/spyder-ide/qtpy/issues/110) - Add tests for untested modules +* [Issue 101](https://github.com/spyder-ide/qtpy/issues/101) - Missing: QtOpenGL Module +* [Issue 89](https://github.com/spyder-ide/qtpy/issues/89) - QDesktopServices split into QDesktopServices and QStandardPaths +* [Issue 57](https://github.com/spyder-ide/qtpy/issues/57) - qInstallMessageHandler <-> qInstallMsgHandler +* [Issue 15](https://github.com/spyder-ide/qtpy/issues/15) - Feature Request: PySide2 support + +In this release 7 issues were closed + +**Pull requests** + +* [PR 125](https://github.com/spyder-ide/qtpy/pull/125) - PR: Fix typo in Readme. +* [PR 117](https://github.com/spyder-ide/qtpy/pull/117) - PR: Add compatibility for the rename of qInstallMsgHandler to qInstallMessageHandler +* [PR 115](https://github.com/spyder-ide/qtpy/pull/115) - PR: Update Readme to reflect that we actually use the PySide2 layout +* [PR 114](https://github.com/spyder-ide/qtpy/pull/114) - PR: Update Readme to mention that we now support PySide2. +* [PR 113](https://github.com/spyder-ide/qtpy/pull/113) - PR: Add tests for Qtdesigner, QtNetwork, QtPrintSupport, QtSvg and QtTest. +* [PR 112](https://github.com/spyder-ide/qtpy/pull/112) - PR: Follow QStandardPaths location in Qt5 for PyQt4/PySide +* [PR 109](https://github.com/spyder-ide/qtpy/pull/109) - PR: Add a coveragerc file +* [PR 106](https://github.com/spyder-ide/qtpy/pull/106) - PR: Add support for PyQt 4.6 +* [PR 102](https://github.com/spyder-ide/qtpy/pull/102) - PR: Add a new QtOpenGL module +* [PR 84](https://github.com/spyder-ide/qtpy/pull/84) - PR: Add PySide2 support + +In this release 10 pull requests were merged + + +---- + + +## Version 1.2.1 (2017/01/21) + +### Bugs fixed + +**Pull requests** + +* [PR 98](https://github.com/spyder-ide/qtpy/pull/98) - PR: Don't use Travis to test macOS because it slows down the entire spyder-ide organization +* [PR 97](https://github.com/spyder-ide/qtpy/pull/97) - PR: Update Appveyor badge in Readme because of moving to an org account +* [PR 94](https://github.com/spyder-ide/qtpy/pull/94) - PR: Include test suite in sdist + +In this release 3 pull requests were merged + + +---- + + +## Version 1.2 (2017/01/08) + +### New features + +* Add support for QtMultimedia +* Use relative imports so its vendored more easily + +### Bugs fixed + +**Issues** + +* [Issue 83](https://github.com/spyder-ide/qtpy/issues/83) - Include core doc files in PyPi releases +* [Issue 78](https://github.com/spyder-ide/qtpy/issues/78) - Request for a new bugfix release +* [Issue 75](https://github.com/spyder-ide/qtpy/issues/75) - Missing copyright headers +* [Issue 67](https://github.com/spyder-ide/qtpy/issues/67) - uic.loadUiType is missing +* [Issue 64](https://github.com/spyder-ide/qtpy/issues/64) - QHeaderView.setSectionResizeMode +* [Issue 49](https://github.com/spyder-ide/qtpy/issues/49) - QtMultimedia support + +In this release 6 issues were closed + +**Pull requests** + +* [PR 93](https://github.com/spyder-ide/qtpy/pull/93) - Restore uic full namespace for PyQt5 and PyQt4 +* [PR 92](https://github.com/spyder-ide/qtpy/pull/92) - Add missing copyright header in _patch/qheaderview.py +* [PR 91](https://github.com/spyder-ide/qtpy/pull/91) - Use star imports in QtSvg again instead of direct ones (reverts PR #55) +* [PR 88](https://github.com/spyder-ide/qtpy/pull/88) - PR: Add manifest +* [PR 74](https://github.com/spyder-ide/qtpy/pull/74) - Move QStringListModel to QtCore +* [PR 71](https://github.com/spyder-ide/qtpy/pull/71) - PR: Use relative imports so its vendored more easily +* [PR 65](https://github.com/spyder-ide/qtpy/pull/65) - Introduce renamed methods of QHeaderView in PyQt4 and PySide +* [PR 59](https://github.com/spyder-ide/qtpy/pull/59) - Don't install qtpy as a conda package in CircleCI +* [PR 58](https://github.com/spyder-ide/qtpy/pull/58) - Remove reference to how qtpy is pronounced in README +* [PR 55](https://github.com/spyder-ide/qtpy/pull/55) - PR: Add explicit imports to QtSvg module +* [PR 50](https://github.com/spyder-ide/qtpy/pull/50) - Add support for QtMultimedia + +In this release 11 pull requests were merged + + +---- + + ## Version 1.1.2 (2016-08-08) ### Bugfixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0e4e7396 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,221 @@ +# Contributing Guide + +QtPy is part of the Spyder IDE GitHub org, and is developed with standard GitHub flow. + +If you're not comfortable with at least the basics of ``git`` and GitHub, we recommend reading beginner tutorials such as [GitHub's Git Guide](https://github.com/git-guides/), its [introduction to basic Git commands](https://guides.github.com/introduction/git-handbook/#basic-git) and its [guide to the fork workflow](https://guides.github.com/activities/forking/), or (if you prefer) their [video equivalents](https://www.youtube.com/githubguides). +However, this contributing guide should fill you in on most of the basics you need to know. + +Let us know if you have any further questions, and we look forward to your contributions! + + +## Reporting Issues + +Discover a bug? +Want a new feature? +[Open](https://github.com/spyder-ide/qtpy/issues/new/choose) an [issue](https://github.com/spyder-ide/qtpy/issues)! +Make sure to describe the bug or feature in detail, with reproducible examples and references if possible, what you are looking to have fixed/added. +While we can't promise we'll fix everything you might find, we'll certainly take it into consideration, and typically welcome pull requests to resolve accepted issues. + + +## Setting Up a Development Environment + +**Note**: You may need to substitute ``python3`` for ``python`` in the commands below on some Linux distros where ``python`` isn't mapped to ``python3`` (yet). + + +### Fork and clone the repo + +First, navigate to the [project repository](https://github.com/spyder-ide/qtpy) in your web browser and press the ``Fork`` button to make a personal copy of the repository on your own GitHub account. +Then, click the ``Clone or Download`` button on your repository, copy the link and run the following on the command line to clone the repo: + +```bash +git clone +``` + +Finally, set the upstream remote to the official QtPy repo with: + +```bash +git remote add upstream https://github.com/spyder-ide/qtpy.git +``` + + +### Create and activate a fresh environment + +Particularly for development installs, we highly recommend you create and activate a virtual environment to avoid any conflicts with other packages on your system or causing any other issues. +Of course, you're free to use any environment management tool of your choice (conda, virtualenvwrapper, pyenv, etc). + +To do so with Conda (recommended), simply execute the following: + +```bash +conda create -c conda-forge -n qtpy-env python=3.9 +``` + +And activate it with + +```bash +conda activate qtpy-env +``` + +With pip/venv, you can create a virtual environment with + +```bash +python -m venv qtpy-env +``` + +And activate it with the following on Linux and macOS, + +```bash +source qtpy-env/bin/activate +``` + +or on Windows (cmd), + +```cmd +.\qtpy-env\Scripts\activate.bat +``` + +Regardless of the tool you use, make sure to remember to always activate your environment before using it. + + +### Install a Python Qt binding + +Before installing QtPy itself, make sure you have the Qt binding(s) you wish to develop against. +For example, for PyQt5 on Conda, you'd run: + +```bash +conda install -c conda-forge pyqt=5 +``` + +Or for the same using pip, you'd execute: + +```bash +python -m pip install pyqt5==5.* PyQtWebEngine==5.* +``` + +While having separate environments for each binding is recommended, you can install multiple in one environment and select between them using the ``QT_API`` environment variable, as described in the [Readme](https://github.com/spyder-ide/qtpy/blob/master/README.md) (for example, setting it to ``pyqt5`` to select PyQt5, if it is installed). + + +### Install QtPy in editable mode + +Finally, to install the QtPy package itself in editable ("development") mode, where updates to the source files will be reflected in the installed package, and include any additional dependencies used for development, run + +```bash +python -m pip install -e .[test] +``` + +You can then import and use QtPy as normal. +When you make changes in your local copy of the git repository, they will be reflected in your installed copy as soon as you re-run Python. + + +### Pre-commit hooks + +We use [pre-commit](https://pre-commit.com/) to run some checks before each commit. To install it in local environment, run: + +```bash +pip install pre-commit +``` + +or globally with pipx: + +```bash +pipx install pre-commit +``` + +or from conda: + +```bash +conda install -c conda-forge pre-commit +``` + +Then, install the pre-commit hooks with: + +```bash +pre-commit install +``` + +If you do not want to run the hooks locally the `pre-commit.ci` workflow will run them for you on GitHub. + + +## Deciding Which Branch to Use + +When you start to work on a new pull request (PR), you need to be sure that your work is done on top of the correct branch, and that you base your PR on GitHub against it. + +To guide you, issues on GitHub are marked with a milestone that indicates the correct branch to use. +If not, follow these guidelines: + +* Use the latest release branch (e.g. ``1.x``) to fix security issues and critical bugs only (if in any doubt, ask first) +* Use ``master`` branch for anything else, particularly introducing new features or breaking compatibility with previous versions + +Of course, if a bug is only present in ``master``, please base bugfixes on that branch. + + +## Making Your Changes + +To start working on a new PR, you need to execute these commands, filling in the branch names where appropriate (```` is the branch you're basing your work against, e.g. ``master``, while ```` is the branch you'll be creating to store your changes, e.g. ``fix-startup-bug`` or ``add-widget-support``: + +```bash +git checkout +git pull upstream +git checkout -b +``` + +Once you've made and tested your changes, commit them with a descriptive, unique message of 74 characters or fewer written in the imperative tense, with a capitalized first letter and no period at the end. +Try to make your commit message understandable on its own, giving the reader a high-level idea of what your changes accomplished without having to dig into the diffs. +For example: + +```bash +git commit -am "Fix bug reading env variable when importing package on Windows" +``` + +If your changes are complex (more than a few dozen lines) and can be broken into discrete steps/parts, it's often a good idea to make multiple commits as you work. +On the other hand, if your changes are fairly small (less than a dozen lines), it's usually better to make them as a single commit, and then use the ``git -a --amend`` (followed by ``git push -f``, if you've already pushed your work) if you spot a bug or a reviewer requests a change. + +These aren't hard and fast rules, so just use your best judgment, and if there does happen to be a significant issue we'll be happy to help. + + +## Running the Tests + +Once you've made your changes (or ideally, before), you'll want to run the full test suite and write new tests of your own, if you haven't already done so. + +This package uses the [Pytest](https://pytest.org) framework for its unit and integration tests, which are located inside the ``tests/`` directory. +We **strongly** suggest you run the full test suite before every commit (it should only take a few seconds to run on most machines). + +In general, any new major functionality should come with tests, and we welcome contributing to expand our coverage, increase reliability, and ensure we don't experience any regressions. +If you need help writing tests, please let us know, and we'll be happy to guide you. + +To run the tests, install the development dependencies as above, and then simply execute + +```bash +pytest +``` + +The ``pytest.ini`` config file configures a variety of settings and command line options for you, so you shouldn't need to pass any further options to pytest unless you have a specific use case. +For a more rigorous run mirroring what is executed on our CIs, execute the following: + +```bash +cd qtpy +python -bb -X dev -W error -m pytest +cd .. +``` + + +## Pushing your Changes + +Now that your changes are ready to go, you'll need to push them to the appropriate remote. +All contributors, including core developers, should push to their personal fork and submit a PR from there, to avoid cluttering the upstream repo with feature branches. +To do so, run: + +```bash +git push -u origin +``` + +Where ```` is the name of your feature branch, e.g. ``fix-startup-bug``. + + +## Submitting a Pull Request + +Finally, create a pull request to the [spyder-ide/qtpy repository](https://github.com/spyder-ide/qtpy/) on GitHub. +Make sure to set the target branch to the one you based your PR off of (``master`` or ``X.x``). + +We'll then review your changes, and after they're ready to go, your work will become an official part of QtPy. + +Thanks for taking the time to read and follow this guide, and we look forward to your contributions! diff --git a/LICENSE.txt b/LICENSE.txt index 4152f523..fed9ab7d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,22 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 2015 Gonzalo Peña-Castellanos - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +The MIT License (MIT) + +Copyright (c) 2011- QtPy contributors and others (see AUTHORS.md) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..3be877b8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include CHANGELOG.md +include SECURITY.md +include pytest.ini +recursive-include tests *.py *.ui diff --git a/README.md b/README.md index fefb604c..628b185a 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,269 @@ -# QtPy: Abtraction layer for PyQt5/PyQt4/PySide +# QtPy: Abstraction layer for PyQt5/PySide2/PyQt6/PySide6 -Copyright © 2009- The Spyder Development Team. - -## Project details [![license](https://img.shields.io/pypi/l/qtpy.svg)](./LICENSE) -[![pypi version](https://img.shields.io/pypi/v/qtpy.svg)](https://pypi.python.org/pypi/qtpy) -[![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public) - -## Build status -[![Travis status](https://travis-ci.org/spyder-ide/qtpy.svg?branch=master)](https://travis-ci.org/spyder-ide/qtpy) -[![Build status](https://ci.appveyor.com/api/projects/status/ab01a09cbx3m0ao9?svg=true)](https://ci.appveyor.com/project/goanpeca/qtpy) -[![CircleCI](https://circleci.com/gh/spyder-ide/qtpy.svg?style=shield)](https://circleci.com/gh/spyder-ide/qtpy) +[![pypi version](https://img.shields.io/pypi/v/qtpy.svg)](https://pypi.org/project/QtPy/) +[![conda version](https://img.shields.io/conda/vn/conda-forge/qtpy.svg)](https://www.anaconda.com/download/) +[![download count](https://img.shields.io/conda/dn/conda-forge/qtpy.svg)](https://www.anaconda.com/download/) +[![OpenCollective Backers](https://opencollective.com/spyder/backers/badge.svg?color=blue)](#sponsors) +[![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public)
+[![PyPI status](https://img.shields.io/pypi/status/qtpy.svg)](https://github.com/spyder-ide/qtpy) +[![Github build status](https://github.com/spyder-ide/qtpy/workflows/Tests/badge.svg)](https://github.com/spyder-ide/qtpy/actions) [![Coverage Status](https://coveralls.io/repos/github/spyder-ide/qtpy/badge.svg?branch=master)](https://coveralls.io/github/spyder-ide/qtpy?branch=master) -[![Code Issues](https://www.quantifiedcode.com/api/v1/project/c769241c7d7f4463b1e6f67863dabace/badge.svg)](https://www.quantifiedcode.com/app/project/c769241c7d7f4463b1e6f67863dabace) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/spyder-ide/qtpy/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/spyder-ide/qtpy/?branch=master) -## Description +*Copyright © 2009– The Spyder Development Team* -**QtPy** is a small abstraction layer that lets you -write applications using a single API call to either PyQt or PySide. -It provides support for PyQt5, PyQt4 and PySide using the PyQt5 layout (where -the QtGui module has been split into QtGui and QtWidgets). +## Description -Basically, you write your code as if you were using PyQt5 but import Qt modules -from `qtpy` instead of `PyQt5`. +**QtPy** provides a uniform interface to support PyQt5, PySide2, PyQt6, and PySide6 through a single codebase. +It abstracts the differences between bindings and versions, allowing software to remain portable and easier to maintain. +Import from `qtpy` instead of PySide/PyQt: -### Attribution and acknowledgements +```python +from qtpy import QtCore, QtWidgets -This project is based on the [pyqode.qt](https://github.com/pyQode/pyqode.qt) -project and the [spyderlib.qt](https://github.com/spyder-ide/spyder/tree/2.3/spyderlib/qt) -module from the [Spyder](https://github.com/spyder-ide/spyder) project, and -also includes contributions adapted from -[qt-helpers](https://github.com/glue-viz/qt-helpers), developed as part of the -[glue](http://glueviz.org) project. +app = QtWidgets.QApplication() +widget = QtWidgets.QWidget() +widget.show() +app.exec() +``` -Unlike `pyqode.qt` this is not a namespace package, so it is not tied -to a particular project or namespace. +## Installation -### License +```shell +pip install qtpy +``` + +or -This project is licensed under the MIT license. +```shell +conda install qtpy +``` ### Requirements -You need PyQt5, PyQt4 or PySide installed in your system to make use -of QtPy. If several of these packages are found, PyQt5 is used by -default unless you set the `QT_API` environment variable. +The installation requires one of the supported packages (PyQt5, PyQt6, PySide2, PySide6) as QtPy does not install any binding by itself. +If multiple of these libraries are found, PyQt5 is used by default. +To set a specific binding, see [Bindings](#Bindings). + + +## Features + +* Supports multiple Qt bindings (PyQt5/6, PySide2/6) without requiring conditional imports or logic branching. +* Detects and loads the available Qt binding automatically based on what is installed or already imported. +* Normalizes the module structure to follow the Qt5 layout (`QtGui` / `QtWidgets`). +* Simplifies the process of porting applications between Qt5 and Qt6 by handling API incompatibilities internally. +* Enables incremental updates to project modules rather than requiring a full-scale rewrite when changing Qt providers. + + +### Bindings + +To set a specific binding, set the `QT_API` environment variable to one of the following values: + +| Value | Binding | +|-----------|---------| +| `pyside6` | PySide6 | +| `pyside2` | PySide2 | +| `pyqt6` | PyQt6 | +| `pyqt5` | PyQt5 | + +For example, to use PyQt6: +```python +import os +os.environ['QT_API'] = 'pyqt6' +from qtpy import QtCore, QtGui, QtWidgets +print(QtWidgets.QWidget) +``` + + +### Module Aliases -`QT_API` can take the following values: +QtPy provides aliases following the Qt5 module layout: -* `pyqt5` (to use PyQt5). -* `pyqt` or `pyqt4` (to use PyQt4). -* `pyside` (to use PySide). +| PyQt5/6 | Alias | +|-----------------------|-------------------| +| `QtCore.pyqtSignal` | `QtCore.Signal` | +| `QtCore.pyqtSlot` | `QtCore.Slot` | +| `QtCore.pyqtProperty` | `QtCore.Property` | +* For PyQt6 enums, unscoped enum access was added by promoting the enums of the `QtCore`, `QtGui`, `QtTest` and `QtWidgets` modules. + +* Compatibility is added between the `QtGui` and `QtOpenGL` modules for the `QOpenGL*` classes. + +For example: +```python +from qtpy import QtCore, QtWidgets + +class Widget(QtWidgets.QWidget): + value_changed = QtCore.Signal(int) +``` + + +### Module Constants + +| Constant | Value | +|-----------------------|-------------------------------------------------------------| +| `QtCore.__version__` | The current Qt version. | +| `qtpy.QT_VERSION` | The current Qt version. | +| `qtpy.PYSIDE_VERSION` | The current binding version for PySide. `None` if not used. | +| `qtpy.PYQT_VERSION` | The current binding version for PyQt. `None` if not used. | +| `qtpy.API_NAME` | The current selected binding. | +| `qtpy.PYSIDE2` | `True`/`False` if PySide2 is currently used. | +| `qtpy.PYSIDE6` | `True`/`False` if PySide6 is currently used. | +| `qtpy.PYQT5` | `True`/`False` if PyQt5 is currently used. | +| `qtpy.PYQT6` | `True`/`False` if PyQt6 is currently used. | +| `qtpy.QT5` | `True`/`False` if Qt5 is currently used. | +| `qtpy.QT6` | `True`/`False` if Qt6 is currently used. | + + +### Compat Module + +The `qtpy.compat` module provides wrappers for `QFileDialog` static methods and SIP/Shiboken functions. + +| Source | Wrappers | +|--------------------------------------|------------------------------------| +| `QFileDialog.getExistingDirectory` | `qtpy.compat.getexistingdirectory` | +| `QFileDialog.getOpenFileName` | `qtpy.compat.getopenfilename` | +| `QFileDialog.getOpenFileNames` | `qtpy.compat.getopenfilenames` | +| `QFileDialog.getSaveFileName` | `qtpy.compat.getsavefilename` | +| `sip.isdeleted` / `shiboken.isValid` | `qtpy.compat.isalive` | + + +### Type Checker Integration + +Type checkers have no knowledge of installed packages, so these tools require additional configuration. + +A Command Line Interface (CLI) is offered to help with usage of QtPy (to get MyPy and Pyright/Pylance args/configurations). + + +#### Mypy + +The `mypy-args` command generates command line arguments for Mypy that will enable it to process the QtPy source files with the same API as QtPy itself would have selected. + +To output a string of Mypy CLI args that will reflect the currently selected Qt API run: + +qtpy mypy-args +``` -### Installation +For example, in an environment where PyQt5 is installed and selected (or the default fallback, if no binding can be found in the environment), this would output the following: + +```text +--always-true=PYQT5 --always-false=PYSIDE2 --always-false=PYQT6 --always-false=PYSIDE6 +``` + +Using Bash or a similar shell, this can be injected into the Mypy command line invocation: ```bash -pip install qtpy +mypy --package mypackage $(qtpy mypy-args) ``` -or +#### Pyright/Pylance + +In the case of Pyright, instead of runtime arguments you need to create a `pyrightconfig.json` config file for the project or a `pyright` section in `pyproject.toml`. +Refer to the [Pyright Configuration](https://github.com/microsoft/pyright/blob/main/docs/configuration.md) for a full reference. +In order to set this configuration, QtPy offers the `pyright-config` command for guidance. + +To print the necessary configuration for your `pyrightconfig.json` or `pyproject.toml`, run: ```bash -conda install qtpy +qtpy pyright-config ``` + +If you don't have either of these, you should create them. + +For example, in an environment where PyQt5 is installed and selected (or the default fallback, if no binding can be found in the environment), `qtpy pyright-config` would output the following: + +`pyrightconfig.json` + +```json +{ + "defineConstant": { + "PYQT5": true, + "PYSIDE2": false, + "PYQT6": false, + "PYSIDE6": false + } +} +``` + +`pyproject.toml` + +```toml +[tool.pyright.defineConstant] +PYQT5 = true +PYSIDE2 = false +PYQT6 = false +PYSIDE6 = false +``` + +**Note**: This configuration is necessary for the correct usage of the default VSCode type checking feature when using QtPy in your source code. + + +## Testing Matrix + +Currently, QtPy runs tests for different bindings on Linux, Windows and macOS, using Python 3.9, 3.11 and 3.13, and installed via `conda` and `pip`. +For the PyQt bindings, the installation of extra packages is also checked via `pip`. + +The current test matrix looks something like this: + +| | Python | 3.9 | | 3.11 | | 3.13 | | +|---------|-------------------|--------------------|------|--------------------|----------------------------|--------------------|----------------------------| +| OS | Binding / manager | conda | pip | conda | pip | conda | pip | +| Linux | PyQt5 | 5.12 | 5.15 | 5.15 | 5.15 (with extras) | 5.15 | 5.15 | +| | PyQt6 | skip (unavailable) | 6.5 | skip (unavailable) | 6.8 (with extras) | skip (unavailable) | 6.8 | +| | PySide2 | 5.13 | 5.12 | 5.15 | skip (no wheels available) | skip (unavailable) | skip (no wheels available) | +| | PySide6 | 6.5 | 6.5 | 6.8 | 6.8 | 6.8 | 6.8 | +| Windows | PyQt5 | 5.12 | 5.15 | 5.15 | 5.15 (with extras) | 5.15 | 5.15 | +| | PyQt6 | skip (unavailable) | 6.2 | skip (unavailable) | 6.8 (with extras) | skip (unavailable) | 6.8 | +| | PySide2 | 5.13 | 5.15 | 5.15 | skip (no wheels available) | skip (unavailable) | skip (no wheels available) | +| | PySide6 | 6.5 | 6.2 | 6.8 | 6.8 | 6.8 | 6.8 | +| MacOS | PyQt5 | 5.12 | 5.15 | skip | 5.15 (with extras) | 5.15 | 5.15 | +| | PyQt6 | skip (unavailable) | 6.2 | skip | 6.8 (with extras) | skip (unavailable) | 6.8 | +| | PySide2 | 5.13 | 5.12 | skip | skip (no wheels available) | skip (unavailable) | skip (no wheels available) | +| | PySide6 | 6.8 | 6.2 | skip | 6.8 | 6.8 | 6.8 | + +**Note**: The mentioned extra packages for the PyQt bindings are the following: + +* `PyQt3D` and `PyQt6-3D` +* `PyQtChart` and `PyQt6-Charts` +* `PyQtDataVisualization` and `PyQt6-DataVisualization` +* `PyQtNetworkAuth` and `PyQt6-NetworkAuth` +* `PyQtPurchasing` +* `PyQtWebEngine` and `PyQt6-WebEngine` +* `QScintilla` and `PyQt6-QScintilla` + + +## Attribution and Acknowledgments + +This project is based on the [pyqode.qt](https://github.com/pyQode/pyqode.qt) project and the [spyderlib.qt](https://github.com/spyder-ide/spyder/tree/2.3/spyderlib/qt) module from the [Spyder](https://github.com/spyder-ide/spyder) project, and also includes contributions adapted from [qt-helpers](https://github.com/glue-viz/qt-helpers), developed as part of the [glue](http://glueviz.org) project. + +Unlike `pyqode.qt` this is not a namespace package, so it is not tied to a particular project or namespace. + + +## Success Stories + +You can check out examples of how QtPy adds support for multiple bindings and allows for incremental updates to new binding versions here: + +* [git-cola](https://github.com/git-cola/git-cola/issues/232) +* [spyder](https://github.com/spyder-ide/spyder) + + +## License + +This project is released under the [MIT license](LICENSE.txt). + + +## Contributing + +Everyone is welcome to contribute! +See our [Contributing guide](CONTRIBUTING.md) for more details. + + +## Sponsors + +QtPy is funded thanks to the generous support of our users from around the world through [Open Collective](https://opencollective.com/spyder/) and [NumFOCUS](https://numfocus.org/project/spyder): + +[![Sponsors](https://opencollective.com/spyder/sponsors.svg)](https://opencollective.com/spyder#support) diff --git a/RELEASE.md b/RELEASE.md index 0aa7e98f..7f622790 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,23 +1,114 @@ -To release a new version of qtpy on PyPI: +# Release Procedure -* git pull +In the commands below, replace `X.Y.Z` with the release version when needed. -* Update CHANGELOG.md +**Note**: We use `pip` instead of `conda` here even on Conda installs, to ensure we always get the latest upstream versions of the build dependencies. -* Update `_version.py` (set release version, remove 'dev0') -* git add and git commit +## PyPI -* python setup.py sdist upload +To release a new version of QtPy on PyPI: -* python setup.py bdist_wheel upload -* git tag -a vX.X.X -m 'comment' +### Prepare -* Update `_version.py` (add 'dev0' and increment minor) +* Close [GitHub milestone](https://github.com/spyder-ide/qtpy/milestones) and ensure all issues are resolved/moved -* git add and git commit +* Update local repo -* git push + ```bash + git restore . && git switch master && git pull upstream master + ``` -* git push --tags +* Clean local repo + + ```bash + git clean -xfdi + ``` + + +### Commit + +* Install/upgrade Loghub + + ```bash + pip install --upgrade loghub + ``` + +* Update `CHANGELOG.md` using Loghub to generate the list of issues and PRs merged to add at the top of the file + + ```bash + loghub -m vX.Y.Z spyder-ide/qtpy + ``` + +* Update `__version__` in `__init__.py` (set release version, remove `.dev0`) + +* Create release commit + + ```bash + git commit -am "Release X.Y.Z" + ``` + + +### Build + +* Update the packaging stack + + ```bash + python -m pip install --upgrade pip + pip install --upgrade --upgrade-strategy eager build setuptools twine wheel + ``` + +* Build source distribution and wheel + + ```bash + python -bb -X dev -W error -m build + ``` + +* Check distribution archives + + ```bash + twine check --strict dist/* + ``` + + +### Release + +* Upload distribution packages to PyPI + + ```bash + twine upload dist/* + ``` + +* Create release tag + + ```bash + git tag -a vX.Y.Z -m "Release X.Y.Z" + ``` + + +### Finalize + +* Update `__version__` in `__init__.py` (add `.dev0` and increment minor) + +* Create `Back to work` commit + + ```bash + git commit -am "Back to work" + ``` + +* Push new release commits and tags to `master` + + ```bash + git push upstream master --follow-tags + ``` + +* Create a [GitHub release](https://github.com/spyder-ide/qtpy/releases) from the tag + + +## Conda-Forge + +To release a new version of QtPy on Conda-Forge: + +* After the release on PyPI, an automatic PR in the [Conda-Forge feedstock repo for QtPy](https://github.com/conda-forge/qtpy-feedstock/pulls) should open. + Merging this PR will update the respective Conda-Forge package. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b2091be3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policy + + +## Supported Versions + +We normally support only the most recently released version with bug fixes, security updates and compatibility improvements. + +The following summarizes the support status of recent QtPy versions. + +| Version | Supported | +|---------|--------------------| +| 2.0.x | :heavy_check_mark: | +| <2 | :x: | + + +## Reporting a Vulnerability + +If you believe you've discovered a security vulnerability in QtPy, please use open a new security advisory with [our GitHub repo's private vulnerability reporting](https://github.com/spyder-ide/qtpy/security/advisories/new). +Please be sure to carefully document the vulnerability, including a summary, describing the impacts, identifying the line(s) of code affected, stating the conditions under which it is exploitable and including a minimal reproducible test case. +Further information and advice or patches on how to mitigate it is always welcome. +You can usually expect to hear back within 1 week, at which point we'll inform you of our evaluation of the vulnerability and what steps we plan to take, and will reach out if we need further clarification from you. +We'll discuss and update the advisory thread, and are happy to update you on its status should you further inquire. +While this is a volunteer project, and we don't have financial compensation to offer, we can certainly publicly thank and credit you for your help if you would like. +Thanks! diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 52ce588e..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,48 +0,0 @@ -# https://ci.appveyor.com/project/goanpeca/qtpy - -branches: - only: - - master - -environment: - global: - PYTHON: "C:\\conda" - CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci-helpers\\appveyor\\windows_sdk.cmd" - PYTHON_ARCH: "64" # needs to be set for CMD_IN_ENV to succeed. If a mix - # of 32 bit and 64 bit builds are needed, move this - # to the matrix section. - # Used by atropy ci-helpers - CONDA_CHANNELS: "spyder-ide qttesting" - - matrix: - # Qt4 - - PYTHON_VERSION: "2.7" - CONDA_DEPENDENCIES: "pytest pytest-cov qt=4.* pyside" - - PYTHON_VERSION: "2.7" - CONDA_DEPENDENCIES: "pytest pytest-cov qt=4.* pyqt=4.*" - - PYTHON_VERSION: "3.4" - CONDA_DEPENDENCIES: "pytest pytest-cov qt=4.* pyqt=4.*" - - PYTHON_VERSION: "3.5" - CONDA_DEPENDENCIES: "pytest pytest-cov qt=4.* pyqt=4.*" - # Qt5 - - PYTHON_VERSION: "2.7" - CONDA_DEPENDENCIES: "pytest pytest-cov qt=5.* pyqt=5.*" - - PYTHON_VERSION: "3.5" - CONDA_DEPENDENCIES: "pytest pytest-cov qt=5.* pyqt=5.*" - -platform: - -x64 - -install: - - "git clone git://github.com/astropy/ci-helpers.git" - - "powershell ci-helpers/appveyor/install-miniconda.ps1" - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "activate test" - - "python setup.py install" - -# Not a .NET project, we build in the install step instead -build: false - -test_script: - - "%CMD_IN_ENV% python qtpy/tests/runtests.py" - diff --git a/circle.yml b/circle.yml deleted file mode 100644 index ef97c02c..00000000 --- a/circle.yml +++ /dev/null @@ -1,34 +0,0 @@ -# https://circleci.com/gh/spyder-ide/qtpy/ - -machine: - environment: - # Used by test scripts - TEST_CI: "True" - PYTHON_TEST: "$HOME/miniconda/envs/test/bin/python" - PYTEST: "$HOME/miniconda/envs/test/bin/py.test" - PATH: "$HOME/miniconda/bin:$PATH" # To avoid prepending this to the commands on circle-ci - # Python versions to test (Maximum of 4 different versions for now) - PY_VERSIONS: "2.7 3.4 3.5" - # Used by astropy-ci helpers - TRAVIS_OS_NAME: "linux" - CONDA_CHANNELS: "qttesting" - CONDA_DEPENDENCIES: "pyqt pytest pytest-cov qt" - PIP_DEPENDENCIES: "coveralls" - -dependencies: - override: - # First convert PY_VERSIONS to an array and then select the Python version based on the - # CIRCLE_NODE_INDEX - - PY_VERSIONS=($PY_VERSIONS) && - TRAVIS_PYTHON_VERSION=${PY_VERSIONS[$CIRCLE_NODE_INDEX]} && - echo -e "PYTHON = $TRAVIS_PYTHON_VERSION \n============" && - git clone git://github.com/astropy/ci-helpers.git && - source ci-helpers/travis/setup_conda_$TRAVIS_OS_NAME.sh && - "$PYTHON_TEST" setup.py install; - -test: - override: - - conda info --json: # note the colon - parallel: true - - "$PYTHON_TEST qtpy/tests/runtests.py": # note the colon - parallel: true diff --git a/conda.recipe/build.sh b/conda.recipe/build.sh deleted file mode 100644 index 2981b577..00000000 --- a/conda.recipe/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -$PYTHON setup.py install --single-version-externally-managed --root=/ diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml deleted file mode 100644 index 0b25c5cd..00000000 --- a/conda.recipe/meta.yaml +++ /dev/null @@ -1,28 +0,0 @@ -package: - name: qtpy - version: {{ GIT_DESCRIBE_TAG|replace('v', '') }} - -build: - noarch_python: True - number: {{ GIT_DESCRIBE_NUMBER|int }} - -source: - git_url: ../ - -requirements: - build: - - python - - setuptools - run: - - python - -test: - imports: - - qtpy - requires: - - pyqt - -about: - home: https://github.com/spyder-ide/qtpy - license: MIT - summary: Provides an uniform layer to support PyQt4, PyQt5 and PySide with a single codebase diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..62883487 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,129 @@ +[build-system] +requires = ["setuptools>=42"] +build-backend = "setuptools.build_meta" + +[project] +name = "QtPy" +dynamic = ["version"] +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." +readme = "README.md" +requires-python = ">=3.9" +authors = [ + { name = "Colin Duquesnoy and the Spyder Development Team", email = "spyder.python@gmail.com" } +] +maintainers = [ + { name = "Spyder Development Team and QtPy Contributors", email = "spyder.python@gmail.com" } +] +license = "MIT" +keywords = ["qt", "PyQt5", "PyQt6", "PySide2", "PySide6"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: MacOS X", + "Environment :: Win32 (MS Windows)", + "Environment :: X11 Applications :: Qt", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: User Interfaces", + "Topic :: Software Development :: Widget Sets", +] +dependencies = ["packaging"] + +[project.urls] +"Github" = "https://github.com/spyder-ide/qtpy" +"Bug Tracker" = "https://github.com/spyder-ide/qtpy/issues" +"Parent Project" = "https://www.spyder-ide.org/" + +[project.optional-dependencies] +test = [ + "pytest>=6,!=7.0.0,!=7.0.1", + "pytest-cov>=3.0.0", + "pytest-qt", +] + +[project.scripts] +qtpy = "qtpy.__main__:main" + +[tool.setuptools.dynamic] +version = { attr = "qtpy.__version__" } + +[tool.setuptools.packages.find] +include = ["qtpy"] + +[tool.setuptools.package-data] +qtpy = ["py.typed"] + +[tool.black] +target-version = ['py39', 'py310', 'py311', 'py312'] +skip-string-normalization = true +line-length = 79 +exclude = ''' +( + /( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | examples + | vendored + | _vendor + )/ + | napari/resources/qt.py + | tools/minreq.py +) +''' + +[tool.ruff] +line-length = 79 +target-version = "py39" + +[tool.ruff.lint] +select = [ + "E", "F", "W", # flake8 + "UP", # pyupgrade + "I", # isort + "YTT", #flake8-2020 + "TCH", # flake8-type-checing + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "COM", # flake8-commas + "SIM", # flake8-simplify + "INP", # flake8-no-pep420 + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "TID", # flake8-tidy-imports, replace absolutify import + "TRY", # tryceratops + "ICN", # flake8-import-conventions + "RUF", # ruff specyfic rules +] +ignore = [ + "F403", # star import + "E501", # line too long - black will handle it + "F405", # star import + "F401", # unused import - cannot determine wole list of symbols to export and use in __all__ +] + +[tool.ruff.lint.per-file-ignores] +"qtpy/QtCore.py" = ["F821"] +"qtpy/__init__.py" = ["TRY003", "F811"] +"qtpy/uic.py" = ["TRY003"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..ea043e87 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +addopts = --durations=10 -v -r a --color=yes --code-highlight=yes --strict-config --strict-markers --maxfail 10 --cov-report=term-missing --cov-report=xml +empty_parameter_set_mark = fail_at_collect +filterwarnings = + error + ignore:.*QSqlDatabase\.exec\(:DeprecationWarning +log_auto_indent = True +log_level = INFO +minversion = 6.0 +testpaths = tests +xfail_strict = True diff --git a/qtpy/Qsci.py b/qtpy/Qsci.py new file mode 100644 index 00000000..85fec911 --- /dev/null +++ b/qtpy/Qsci.py @@ -0,0 +1,36 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qsci classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qsci import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qsci", + missing_package="QScintilla", + ) from error +elif PYQT6: + try: + from PyQt6.Qsci import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qsci", + missing_package="PyQt6-QScintilla", + ) from error +elif PYSIDE2 or PYSIDE6: + raise QtBindingMissingModuleError(name="Qsci") diff --git a/qtpy/Qt3DAnimation.py b/qtpy/Qt3DAnimation.py new file mode 100644 index 00000000..08357630 --- /dev/null +++ b/qtpy/Qt3DAnimation.py @@ -0,0 +1,56 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qt3DAnimation classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qt3DAnimation import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DAnimation", + missing_package="PyQt3D", + ) from error +elif PYQT6: + try: + from PyQt6 import Qt3DAnimation + from PyQt6.Qt3DAnimation import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(Qt3DAnimation) + del Qt3DAnimation + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DAnimation", + missing_package="PyQt6-3D", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.Qt3DAnimation as __temp + + for __name in inspect.getmembers(__temp.Qt3DAnimation): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide6.Qt3DAnimation as __temp + + for __name in inspect.getmembers(__temp.Qt3DAnimation): + globals()[__name[0]] = __name[1] diff --git a/qtpy/Qt3DCore.py b/qtpy/Qt3DCore.py new file mode 100644 index 00000000..5c15df19 --- /dev/null +++ b/qtpy/Qt3DCore.py @@ -0,0 +1,49 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qt3DCore classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qt3DCore import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DCore", + missing_package="PyQt3D", + ) from error +elif PYQT6: + try: + from PyQt6.Qt3DCore import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DCore", + missing_package="PyQt6-3D", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.Qt3DCore as __temp + + for __name in inspect.getmembers(__temp.Qt3DCore): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide6.Qt3DCore as __temp + + for __name in inspect.getmembers(__temp.Qt3DCore): + globals()[__name[0]] = __name[1] diff --git a/qtpy/Qt3DExtras.py b/qtpy/Qt3DExtras.py new file mode 100644 index 00000000..d146065d --- /dev/null +++ b/qtpy/Qt3DExtras.py @@ -0,0 +1,49 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qt3DExtras classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qt3DExtras import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DExtras", + missing_package="PyQt3D", + ) from error +elif PYQT6: + try: + from PyQt6.Qt3DExtras import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DExtras", + missing_package="PyQt6-3D", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.Qt3DExtras as __temp + + for __name in inspect.getmembers(__temp.Qt3DExtras): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide6.Qt3DExtras as __temp + + for __name in inspect.getmembers(__temp.Qt3DExtras): + globals()[__name[0]] = __name[1] diff --git a/qtpy/Qt3DInput.py b/qtpy/Qt3DInput.py new file mode 100644 index 00000000..1dbc3b44 --- /dev/null +++ b/qtpy/Qt3DInput.py @@ -0,0 +1,49 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qt3DInput classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qt3DInput import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DInput", + missing_package="PyQt3D", + ) from error +elif PYQT6: + try: + from PyQt6.Qt3DInput import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DInput", + missing_package="PyQt6-3D", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.Qt3DInput as __temp + + for __name in inspect.getmembers(__temp.Qt3DInput): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide6.Qt3DInput as __temp + + for __name in inspect.getmembers(__temp.Qt3DInput): + globals()[__name[0]] = __name[1] diff --git a/qtpy/Qt3DLogic.py b/qtpy/Qt3DLogic.py new file mode 100644 index 00000000..3fea3f51 --- /dev/null +++ b/qtpy/Qt3DLogic.py @@ -0,0 +1,49 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qt3DLogic classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qt3DLogic import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DLogic", + missing_package="PyQt3D", + ) from error +elif PYQT6: + try: + from PyQt6.Qt3DLogic import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DLogic", + missing_package="PyQt6-3D", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.Qt3DLogic as __temp + + for __name in inspect.getmembers(__temp.Qt3DLogic): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide6.Qt3DLogic as __temp + + for __name in inspect.getmembers(__temp.Qt3DLogic): + globals()[__name[0]] = __name[1] diff --git a/qtpy/Qt3DRender.py b/qtpy/Qt3DRender.py new file mode 100644 index 00000000..e995d5f4 --- /dev/null +++ b/qtpy/Qt3DRender.py @@ -0,0 +1,56 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Qt3DRender classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.Qt3DRender import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DRender", + missing_package="PyQt3D", + ) from error +elif PYQT6: + try: + from PyQt6 import Qt3DRender + from PyQt6.Qt3DRender import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(Qt3DRender) + del Qt3DRender + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="Qt3DRender", + missing_package="PyQt6-3D", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.Qt3DRender as __temp + + for __name in inspect.getmembers(__temp.Qt3DRender): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide6.Qt3DRender as __temp + + for __name in inspect.getmembers(__temp.Qt3DRender): + globals()[__name[0]] = __name[1] diff --git a/qtpy/QtAxContainer.py b/qtpy/QtAxContainer.py new file mode 100644 index 00000000..fa2ba4ed --- /dev/null +++ b/qtpy/QtAxContainer.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtAxContainer classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5 or PYQT6: + raise QtBindingMissingModuleError(name="QtAxContainer") +elif PYSIDE2: + from PySide2.QtAxContainer import * +elif PYSIDE6: + from PySide6.QtAxContainer import * diff --git a/qtpy/QtBluetooth.py b/qtpy/QtBluetooth.py new file mode 100644 index 00000000..237f81f9 --- /dev/null +++ b/qtpy/QtBluetooth.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtBluetooth classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.QtBluetooth import * +elif PYQT6: + from PyQt6.QtBluetooth import * +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtBluetooth") +elif PYSIDE6: + from PySide6.QtBluetooth import * diff --git a/qtpy/QtCharts.py b/qtpy/QtCharts.py new file mode 100644 index 00000000..81716b0f --- /dev/null +++ b/qtpy/QtCharts.py @@ -0,0 +1,47 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2019- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtChart classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5 import QtChart as QtCharts + from PyQt5.QtChart import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtCharts", + missing_package="PyQtChart", + ) from error +elif PYQT6: + try: + from PyQt6 import QtCharts + from PyQt6.QtCharts import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtCharts", + missing_package="PyQt6-Charts", + ) from error +elif PYSIDE2: + import inspect + + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import PySide2.QtCharts as __temp + from PySide2.QtCharts import * + + for __name in inspect.getmembers(__temp.QtCharts): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + from PySide6 import QtCharts + from PySide6.QtCharts import * diff --git a/qtpy/QtConcurrent.py b/qtpy/QtConcurrent.py new file mode 100644 index 00000000..a8e3ed3f --- /dev/null +++ b/qtpy/QtConcurrent.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtConcurrent classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5 or PYQT6: + raise QtBindingMissingModuleError(name="QtConcurrent") +elif PYSIDE2: + from PySide2.QtConcurrent import * +elif PYSIDE6: + from PySide6.QtConcurrent import * diff --git a/qtpy/QtCore.py b/qtpy/QtCore.py index 07adafb4..777db3b4 100644 --- a/qtpy/QtCore.py +++ b/qtpy/QtCore.py @@ -1,47 +1,197 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy # Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- -""" -Provides QtCore classes and functions. -""" - -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError +"""Provides QtCore classes and functions.""" +import contextlib +from typing import TYPE_CHECKING +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, _parse_version +from . import QT_VERSION as _qt_version +from ._utils import possibly_static_exec, possibly_static_exec_ if PYQT5: from PyQt5.QtCore import * + from PyQt5.QtCore import pyqtBoundSignal as SignalInstance + from PyQt5.QtCore import pyqtProperty as Property from PyQt5.QtCore import pyqtSignal as Signal from PyQt5.QtCore import pyqtSlot as Slot - from PyQt5.QtCore import pyqtProperty as Property + + try: + from PyQt5.QtCore import Q_ENUM as QEnum + + del Q_ENUM + except ImportError: # fallback for Qt5.9 + from PyQt5.QtCore import Q_ENUMS as QEnum + + del Q_ENUMS from PyQt5.QtCore import QT_VERSION_STR as __version__ # Those are imported from `import *` - del pyqtSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR -elif PYQT4: - from PyQt4.QtCore import * - # Those are things we inherited from Spyder that fix crazy crashes under - # some specific situations. (See #34) - from PyQt4.QtCore import QCoreApplication - from PyQt4.QtCore import Qt - from PyQt4.QtCore import pyqtSignal as Signal - from PyQt4.QtCore import pyqtSlot as Slot - from PyQt4.QtCore import pyqtProperty as Property - from PyQt4.QtGui import (QItemSelection, QItemSelectionModel, - QItemSelectionRange, QSortFilterProxyModel) - from PyQt4.QtCore import QT_VERSION_STR as __version__ + del pyqtSignal, pyqtBoundSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR + +elif PYQT6: + from PyQt6 import QtCore + from PyQt6.QtCore import * + from PyQt6.QtCore import QT_VERSION_STR as __version__ + from PyQt6.QtCore import pyqtBoundSignal as SignalInstance + from PyQt6.QtCore import pyqtEnum as QEnum + from PyQt6.QtCore import pyqtProperty as Property + from PyQt6.QtCore import pyqtSignal as Signal + from PyQt6.QtCore import pyqtSlot as Slot + + # For issue #311 + # Seems like there is an error with sip. Without first + # trying to import `PyQt6.QtGui.Qt`, some functions like + # `PyQt6.QtCore.Qt.mightBeRichText` are missing. + if not TYPE_CHECKING: + with contextlib.suppress(ImportError): + from PyQt6.QtGui import Qt + + # Map missing methods + QCoreApplication.exec_ = lambda *args, **kwargs: possibly_static_exec( + QCoreApplication, + *args, + **kwargs, + ) + QEventLoop.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QThread.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) # Those are imported from `import *` - del pyqtSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR -elif PYSIDE: - from PySide.QtCore import * - from PySide.QtGui import (QItemSelection, QItemSelectionModel, - QItemSelectionRange, QSortFilterProxyModel) - import PySide.QtCore - __version__ = PySide.QtCore.__version__ -else: - raise PythonQtError('No Qt bindings could be found') + del ( + pyqtSignal, + pyqtBoundSignal, + pyqtSlot, + pyqtProperty, + pyqtEnum, + QT_VERSION_STR, + ) + + # Allow unscoped access for enums inside the QtCore module + from .enums_compat import promote_enums + + promote_enums(QtCore) + del QtCore + + # Alias deprecated ItemDataRole enum values removed in Qt6 + Qt.BackgroundColorRole = ( + Qt.ItemDataRole.BackgroundColorRole + ) = Qt.BackgroundRole + Qt.TextColorRole = Qt.ItemDataRole.TextColorRole = Qt.ForegroundRole + + # Alias for MiddleButton removed in PyQt6 but available in PyQt5, PySide2 and PySide6 + Qt.MidButton = Qt.MiddleButton + Qt.MouseButton.MidButton = Qt.MiddleButton + + # Add removed definition for `Qt.ItemFlags` as an alias of `Qt.ItemFlag` + # passing as default value 0 in the same way PySide6 6.5+ does. + # Note that for PyQt5 and PySide2 those definitions are two different classes + # (one is the flag definition and the other the enum definition) + Qt.ItemFlags = lambda value=0: Qt.ItemFlag(value) + +elif PYSIDE2: + import PySide2.QtCore + from PySide2.QtCore import * + + __version__ = PySide2.QtCore.__version__ + + # Missing QtGui utility functions on Qt + if getattr(Qt, "mightBeRichText", None) is None: + try: + from PySide2.QtGui import Qt as guiQt + + Qt.mightBeRichText = guiQt.mightBeRichText + del guiQt + except ImportError: + # Fails with PySide2 5.12.0 + pass + + QCoreApplication.exec = lambda *args, **kwargs: possibly_static_exec_( + QCoreApplication, + *args, + **kwargs, + ) + QEventLoop.exec = lambda self, *args, **kwargs: self.exec_(*args, **kwargs) + QThread.exec = lambda self, *args, **kwargs: self.exec_(*args, **kwargs) + QTextStreamManipulator.exec = lambda self, *args, **kwargs: self.exec_( + *args, + **kwargs, + ) + +elif PYSIDE6: + import PySide6.QtCore + from PySide6.QtCore import * + + __version__ = PySide6.QtCore.__version__ + + # Missing QtGui utility functions on Qt + if getattr(Qt, "mightBeRichText", None) is None: + from PySide6.QtGui import Qt as guiQt + + Qt.mightBeRichText = guiQt.mightBeRichText + del guiQt + + # Alias deprecated ItemDataRole enum values removed in Qt6 + Qt.BackgroundColorRole = ( + Qt.ItemDataRole.BackgroundColorRole + ) = Qt.BackgroundRole + Qt.TextColorRole = Qt.ItemDataRole.TextColorRole = Qt.ForegroundRole + Qt.MidButton = Qt.MiddleButton + Qt.MouseButton.MidButton = Qt.MiddleButton + + # Map DeprecationWarning methods + QCoreApplication.exec_ = lambda *args, **kwargs: possibly_static_exec( + QCoreApplication, + *args, + **kwargs, + ) + QEventLoop.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QThread.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QTextStreamManipulator.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) + + # Passing as default value 0 in the same way PySide6 6.3.2 does for the `Qt.ItemFlags` definition. + if _parse_version(_qt_version) > _parse_version("6.3"): + Qt.ItemFlags = lambda value=0: Qt.ItemFlag(value) + +# For issue #153 and updated for issue #305 +if PYQT5 or PYQT6: + QDate.toPython = lambda self, *args, **kwargs: self.toPyDate( + *args, + **kwargs, + ) + QDateTime.toPython = lambda self, *args, **kwargs: self.toPyDateTime( + *args, + **kwargs, + ) + QTime.toPython = lambda self, *args, **kwargs: self.toPyTime( + *args, + **kwargs, + ) +if PYSIDE2 or PYSIDE6: + QDate.toPyDate = lambda self, *args, **kwargs: self.toPython( + *args, + **kwargs, + ) + QDateTime.toPyDateTime = lambda self, *args, **kwargs: self.toPython( + *args, + **kwargs, + ) + QTime.toPyTime = lambda self, *args, **kwargs: self.toPython( + *args, + **kwargs, + ) + +# Mirror https://github.com/spyder-ide/qtpy/pull/393 +if PYQT5 or PYSIDE2: + QLibraryInfo.path = QLibraryInfo.location + QLibraryInfo.LibraryPath = QLibraryInfo.LibraryLocation +if PYQT6 or PYSIDE6: + QLibraryInfo.location = QLibraryInfo.path + QLibraryInfo.LibraryLocation = QLibraryInfo.LibraryPath diff --git a/qtpy/QtDBus.py b/qtpy/QtDBus.py new file mode 100644 index 00000000..18b2250a --- /dev/null +++ b/qtpy/QtDBus.py @@ -0,0 +1,38 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtDBus classes and functions.""" + +import sys + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, + QtModuleNotInOSError, +) + +if PYQT5: + from PyQt5.QtDBus import * +elif PYQT6: + from PyQt6 import QtDBus + from PyQt6.QtDBus import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtDBus) + del QtDBus +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtDBus") +elif PYSIDE6: + if sys.platform != "win32": + from PySide6.QtDBus import * + else: + raise QtModuleNotInOSError(name="QtDBus") diff --git a/qtpy/QtDataVisualization.py b/qtpy/QtDataVisualization.py new file mode 100644 index 00000000..0a4facf8 --- /dev/null +++ b/qtpy/QtDataVisualization.py @@ -0,0 +1,43 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtDataVisualization classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.QtDataVisualization import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtDataVisualization", + missing_package="PyQtDataVisualization", + ) from error +elif PYQT6: + try: + from PyQt6.QtDataVisualization import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtDataVisualization", + missing_package="PyQt6-DataVisualization", + ) from error +elif PYSIDE2: + # https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1026 + import inspect + + import PySide2.QtDataVisualization as __temp + + for __name in inspect.getmembers(__temp.QtDataVisualization): + globals()[__name[0]] = __name[1] +elif PYSIDE6: + from PySide6.QtDataVisualization import * diff --git a/qtpy/QtDesigner.py b/qtpy/QtDesigner.py index 8b2ddedb..f716db72 100644 --- a/qtpy/QtDesigner.py +++ b/qtpy/QtDesigner.py @@ -1,20 +1,32 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- -""" -Provides QtDesigner classes and functions. -""" - -from qtpy import PYQT5, PYQT4, PythonQtError +"""Provides QtDesigner classes and functions.""" +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) if PYQT5: from PyQt5.QtDesigner import * -elif PYQT4: - from PyQt4.QtDesigner import * -else: - raise PythonQtError('No Qt bindings could be found') +elif PYQT6: + from PyQt6 import QtDesigner + from PyQt6.QtDesigner import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtDesigner) + del QtDesigner +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtDesigner") +elif PYSIDE6: + from PySide6.QtDesigner import * diff --git a/qtpy/QtGui.py b/qtpy/QtGui.py index 84049e57..af5f5ac8 100644 --- a/qtpy/QtGui.py +++ b/qtpy/QtGui.py @@ -1,103 +1,446 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy # Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtGui classes and functions.""" + +from functools import partialmethod + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, _parse_version +from . import QT_VERSION as _qt_version +from ._utils import ( + getattr_missing_optional_dep, + possibly_static_exec, + set_shortcut, + set_shortcuts, +) + +_missing_optional_names = {} -""" -Provides QtGui classes and functions. -.. warning:: Only PyQt4/PySide QtGui classes compatible with PyQt5.QtGui are - exposed here. Therefore, you need to treat/use this package as if it were - the ``PyQt5.QtGui`` module. -""" +_QTOPENGL_NAMES = { + "QOpenGLBuffer", + "QOpenGLContext", + "QOpenGLContextGroup", + "QOpenGLDebugLogger", + "QOpenGLDebugMessage", + "QOpenGLFramebufferObject", + "QOpenGLFramebufferObjectFormat", + "QOpenGLPixelTransferOptions", + "QOpenGLShader", + "QOpenGLShaderProgram", + "QOpenGLTexture", + "QOpenGLTextureBlitter", + "QOpenGLVersionProfile", + "QOpenGLVertexArrayObject", + "QOpenGLWindow", +} -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError + +def __getattr__(name): + """Custom getattr to chain and wrap errors due to missing optional deps.""" + raise getattr_missing_optional_dep( + name, + module_name=__name__, + optional_names=_missing_optional_names, + ) if PYQT5: from PyQt5.QtGui import * -elif PYQT4: - from PyQt4.Qt import QKeySequence, QTextCursor - from PyQt4.QtGui import (QAbstractTextDocumentLayout, QActionEvent, QBitmap, - QBrush, QClipboard, QCloseEvent, QColor, - QConicalGradient, QContextMenuEvent, QCursor, - QDesktopServices, QDoubleValidator, QDrag, - QDragEnterEvent, QDragLeaveEvent, QDragMoveEvent, - QDropEvent, QFileOpenEvent, QFocusEvent, QFont, - QFontDatabase, QFontInfo, QFontMetrics, - QFontMetricsF, QGlyphRun, QGradient, QHelpEvent, - QHideEvent, QHoverEvent, QIcon, QIconDragEvent, - QIconEngine, QImage, QImageIOHandler, QImageReader, - QImageWriter, QInputEvent, QInputMethodEvent, - QKeyEvent, QLinearGradient, - QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, - QMatrix3x3, QMatrix3x4, QMatrix4x2, QMatrix4x3, - QMatrix4x4, QMouseEvent, QMoveEvent, QMovie, - QPaintDevice, QPaintEngine, QPaintEngineState, - QPaintEvent, QPainter, QPainterPath, - QPainterPathStroker, QPalette, QPen, QPicture, - QPictureIO, QPixmap, QPixmapCache, QPolygon, - QPolygonF, QQuaternion, QRadialGradient, QRawFont, - QRegExpValidator, QRegion, QResizeEvent, - QSessionManager, QShortcutEvent, QShowEvent, - QStandardItem, QStandardItemModel, QStaticText, - QStatusTipEvent, QSyntaxHighlighter, QTabletEvent, - QTextBlock, QTextBlockFormat, QTextBlockGroup, - QTextBlockUserData, QTextCharFormat, - QTextDocument, QTextDocumentFragment, - QTextDocumentWriter, QTextFormat, QTextFragment, - QTextFrame, QTextFrameFormat, QTextImageFormat, - QTextInlineObject, QTextItem, QTextLayout, - QTextLength, QTextLine, QTextList, QTextListFormat, - QTextObject, QTextObjectInterface, QTextOption, - QTextTable, QTextTableCell, QTextTableCellFormat, - QTextTableFormat, QTouchEvent, QTransform, - QValidator, QVector2D, QVector3D, QVector4D, - QWhatsThisClickedEvent, QWheelEvent, - QWindowStateChangeEvent, qAlpha, qBlue, - qFuzzyCompare, qGray, qGreen, qIsGray, qRed, qRgb, - qRgba, QIntValidator) -elif PYSIDE: - from PySide.QtGui import (QAbstractTextDocumentLayout, QActionEvent, QBitmap, - QBrush, QClipboard, QCloseEvent, QColor, - QConicalGradient, QContextMenuEvent, QCursor, - QDesktopServices, QDoubleValidator, QDrag, - QDragEnterEvent, QDragLeaveEvent, QDragMoveEvent, - QDropEvent, QFileOpenEvent, QFocusEvent, QFont, - QFontDatabase, QFontInfo, QFontMetrics, - QFontMetricsF, QGradient, QHelpEvent, - QHideEvent, QHoverEvent, QIcon, QIconDragEvent, - QIconEngine, QImage, QImageIOHandler, QImageReader, - QImageWriter, QInputEvent, QInputMethodEvent, - QKeyEvent, QKeySequence, QLinearGradient, - QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, - QMatrix3x3, QMatrix3x4, QMatrix4x2, QMatrix4x3, - QMatrix4x4, QMouseEvent, QMoveEvent, QMovie, - QPaintDevice, QPaintEngine, QPaintEngineState, - QPaintEvent, QPainter, QPainterPath, - QPainterPathStroker, QPalette, QPen, QPicture, - QPictureIO, QPixmap, QPixmapCache, QPolygon, - QPolygonF, QQuaternion, QRadialGradient, - QRegExpValidator, QRegion, QResizeEvent, - QSessionManager, QShortcutEvent, QShowEvent, - QStandardItem, QStandardItemModel, - QStatusTipEvent, QSyntaxHighlighter, QTabletEvent, - QTextBlock, QTextBlockFormat, QTextBlockGroup, - QTextBlockUserData, QTextCharFormat, QTextCursor, - QTextDocument, QTextDocumentFragment, - QTextFormat, QTextFragment, - QTextFrame, QTextFrameFormat, QTextImageFormat, - QTextInlineObject, QTextItem, QTextLayout, - QTextLength, QTextLine, QTextList, QTextListFormat, - QTextObject, QTextObjectInterface, QTextOption, - QTextTable, QTextTableCell, QTextTableCellFormat, - QTextTableFormat, QTouchEvent, QTransform, - QValidator, QVector2D, QVector3D, QVector4D, - QWhatsThisClickedEvent, QWheelEvent, - QWindowStateChangeEvent, qAlpha, qBlue, - qGray, qGreen, qIsGray, qRed, qRgb, qRgba, - QIntValidator) -else: - raise PythonQtError('No Qt bindings could be found') + + # Backport items moved to QtGui in Qt6 + from PyQt5.QtWidgets import ( + QAction, + QActionGroup, + QFileSystemModel, + QShortcut, + QUndoCommand, + QUndoStack, + ) + +elif PYQT6: + from PyQt6 import QtGui + from PyQt6.QtGui import * + + # Attempt to import QOpenGL* classes, but if that fails, + # don't raise an exception until the name is explicitly accessed. + # See https://github.com/spyder-ide/qtpy/pull/387/ + try: + from PyQt6.QtOpenGL import * + except ImportError as error: + for name in _QTOPENGL_NAMES: + _missing_optional_names[name] = { + "name": "PyQt6.QtOpenGL", + "missing_package": "pyopengl", + "import_error": error, + } + + QFontMetrics.width = lambda self, *args, **kwargs: self.horizontalAdvance( + *args, + **kwargs, + ) + QFontMetricsF.width = lambda self, *args, **kwargs: self.horizontalAdvance( + *args, + **kwargs, + ) + + # Map missing/renamed methods + QDrag.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QGuiApplication.exec_ = lambda *args, **kwargs: possibly_static_exec( + QGuiApplication, + *args, + **kwargs, + ) + QTextDocument.print_ = lambda self, *args, **kwargs: self.print( + *args, + **kwargs, + ) + + # Allow unscoped access for enums inside the QtGui module + from .enums_compat import promote_enums + + promote_enums(QtGui) + del QtGui +elif PYSIDE2: + from PySide2.QtGui import * + + # Backport items moved to QtGui in Qt6 + from PySide2.QtWidgets import ( + QAction, + QActionGroup, + QFileSystemModel, + QShortcut, + QUndoCommand, + QUndoStack, + ) + + if hasattr(QFontMetrics, "horizontalAdvance"): + # Needed to prevent raising a DeprecationWarning when using QFontMetrics.width + QFontMetrics.width = ( + lambda self, *args, **kwargs: self.horizontalAdvance( + *args, + **kwargs, + ) + ) +elif PYSIDE6: + from PySide6.QtGui import * + + # Attempt to import QOpenGL* classes, but if that fails, + # don't raise an exception until the name is explicitly accessed. + # See https://github.com/spyder-ide/qtpy/pull/387/ + try: + from PySide6.QtOpenGL import * + except ImportError as error: + for name in _QTOPENGL_NAMES: + _missing_optional_names[name] = { + "name": "PySide6.QtOpenGL", + "missing_package": "pyopengl", + "import_error": error, + } + + # Backport `QFileSystemModel` moved to QtGui in Qt6 + from PySide6.QtWidgets import QFileSystemModel + + QFontMetrics.width = lambda self, *args, **kwargs: self.horizontalAdvance( + *args, + **kwargs, + ) + QFontMetricsF.width = lambda self, *args, **kwargs: self.horizontalAdvance( + *args, + **kwargs, + ) + + # Map DeprecationWarning methods + QDrag.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QGuiApplication.exec_ = lambda *args, **kwargs: possibly_static_exec( + QGuiApplication, + *args, + **kwargs, + ) + +if PYSIDE2 or PYSIDE6: + # PySide{2,6} do not accept the `mode` keyword argument in + # QTextCursor.movePosition() even though it is a valid optional argument + # as per C++ API. Fix this by monkeypatching. + # + # Notes: + # + # * The `mode` argument is called `arg__2` in PySide{2,6} as per + # QTextCursor.movePosition.__doc__ and __signature__. Using `arg__2` as + # keyword argument works as intended, so does using a positional + # argument. Tested with PySide2 5.15.0, 5.15.2.1 and 5.15.3 and PySide6 + # 6.3.0; older version, down to PySide 1, are probably affected as well [1]. + # + # * PySide2 5.15.0 and 5.15.2.1 silently ignore invalid keyword arguments, + # i.e. passing the `mode` keyword argument has no effect and doesn`t + # raise an exception. Older versions, down to PySide 1, are probably + # affected as well [1]. At least PySide2 5.15.3 and PySide6 6.3.0 raise an + # exception when `mode` or any other invalid keyword argument is passed. + # + # [1] https://bugreports.qt.io/browse/PYSIDE-185 + movePosition = QTextCursor.movePosition + + def movePositionPatched( + self, + operation: QTextCursor.MoveOperation, + mode: QTextCursor.MoveMode = QTextCursor.MoveAnchor, + n: int = 1, + ) -> bool: + return movePosition(self, operation, mode, n) + + QTextCursor.movePosition = movePositionPatched + +if PYQT5 or PYSIDE2: + # Part of the fix for https://github.com/spyder-ide/qtpy/issues/394 + from qtpy.QtCore import QPointF as __QPointF + + QNativeGestureEvent.x = lambda self: self.localPos().toPoint().x() + QNativeGestureEvent.y = lambda self: self.localPos().toPoint().y() + QNativeGestureEvent.position = lambda self: self.localPos() + QNativeGestureEvent.globalX = lambda self: self.globalPos().x() + QNativeGestureEvent.globalY = lambda self: self.globalPos().y() + QNativeGestureEvent.globalPosition = lambda self: __QPointF( + float(self.globalPos().x()), + float(self.globalPos().y()), + ) + QEnterEvent.position = lambda self: self.localPos() + QEnterEvent.globalPosition = lambda self: __QPointF( + float(self.globalX()), + float(self.globalY()), + ) + QTabletEvent.position = lambda self: self.posF() + QTabletEvent.globalPosition = lambda self: self.globalPosF() + QHoverEvent.x = lambda self: self.pos().x() + QHoverEvent.y = lambda self: self.pos().y() + QHoverEvent.position = lambda self: self.posF() + # No `QHoverEvent.globalPosition`, `QHoverEvent.globalX`, + # nor `QHoverEvent.globalY` in the Qt5 docs. + QMouseEvent.position = lambda self: self.localPos() + QMouseEvent.globalPosition = lambda self: __QPointF( + float(self.globalX()), + float(self.globalY()), + ) + + # Follow similar approach for `QDropEvent` and child classes + QDropEvent.position = lambda self: self.posF() + +if PYQT6 or PYSIDE6: + # Part of the fix for https://github.com/spyder-ide/qtpy/issues/394 + for _class in ( + QNativeGestureEvent, + QEnterEvent, + QTabletEvent, + QHoverEvent, + QMouseEvent, + ): + for _obsolete_function in ( + "pos", + "x", + "y", + "globalPos", + "globalX", + "globalY", + ): + if hasattr(_class, _obsolete_function): + delattr(_class, _obsolete_function) + QSinglePointEvent.pos = lambda self: self.position().toPoint() + QSinglePointEvent.posF = lambda self: self.position() + QSinglePointEvent.localPos = lambda self: self.position() + QSinglePointEvent.x = lambda self: self.position().toPoint().x() + QSinglePointEvent.y = lambda self: self.position().toPoint().y() + QSinglePointEvent.globalPos = lambda self: self.globalPosition().toPoint() + QSinglePointEvent.globalX = ( + lambda self: self.globalPosition().toPoint().x() + ) + QSinglePointEvent.globalY = ( + lambda self: self.globalPosition().toPoint().y() + ) + + # Follow similar approach for `QDropEvent` and child classes + QDropEvent.pos = lambda self: self.position().toPoint() + QDropEvent.posF = lambda self: self.position() + + +if PYQT5 or PYSIDE2 or _parse_version(_qt_version) < _parse_version("6.4"): + # Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4 + _action_set_shortcut = partialmethod( + set_shortcut, + old_set_shortcut=QAction.setShortcut, + ) + _action_set_shortcuts = partialmethod( + set_shortcuts, + old_set_shortcuts=QAction.setShortcuts, + ) + QAction.setShortcut = _action_set_shortcut + QAction.setShortcuts = _action_set_shortcuts + + +if PYQT5 or PYSIDE2 or _parse_version(_qt_version) < _parse_version("6.7"): + # Make `QIcon.ThemeIcon` enum for Qt < 6.7 + + # Make an `StrEnum` for Python < 3.11 + try: + from enum import StrEnum as _StrEnum + except ImportError: + from enum import Enum as _Enum + + class _StrEnum(str, _Enum): + pass + + class _ThemeIcon(_StrEnum): + AddressBookNew = "address-book-new" + ApplicationExit = "application-exit" + AppointmentNew = "appointment-new" + CallStart = "call-start" + CallStop = "call-stop" + ContactNew = "contact-new" + DocumentNew = "document-new" + DocumentOpen = "document-open" + DocumentOpenRecent = "document-open-recent" + DocumentPageSetup = "document-page-setup" + DocumentPrint = "document-print" + DocumentPrintPreview = "document-print-preview" + DocumentProperties = "document-properties" + DocumentRevert = "document-revert" + DocumentSave = "document-save" + DocumentSaveAs = "document-save-as" + DocumentSend = "document-send" + EditClear = "edit-clear" + EditCopy = "edit-copy" + EditCut = "edit-cut" + EditDelete = "edit-delete" + EditFind = "edit-find" + EditPaste = "edit-paste" + EditRedo = "edit-redo" + EditSelectAll = "edit-select-all" + EditUndo = "edit-undo" + FolderNew = "folder-new" + FormatIndentLess = "format-indent-less" + FormatIndentMore = "format-indent-more" + FormatJustifyCenter = "format-justify-center" + FormatJustifyFill = "format-justify-fill" + FormatJustifyLeft = "format-justify-left" + FormatJustifyRight = "format-justify-right" + FormatTextDirectionLtr = "format-text-direction-ltr" + FormatTextDirectionRtl = "format-text-direction-rtl" + FormatTextBold = "format-text-bold" + FormatTextItalic = "format-text-italic" + FormatTextUnderline = "format-text-underline" + FormatTextStrikethrough = "format-text-strikethrough" + GoDown = "go-down" + GoHome = "go-home" + GoNext = "go-next" + GoPrevious = "go-previous" + GoUp = "go-up" + HelpAbout = "help-about" + HelpFaq = "help-faq" + InsertImage = "insert-image" + InsertLink = "insert-link" + InsertText = "insert-text" + ListAdd = "list-add" + ListRemove = "list-remove" + MailForward = "mail-forward" + MailMarkImportant = "mail-mark-important" + MailMarkRead = "mail-mark-read" + MailMarkUnread = "mail-mark-unread" + MailMessageNew = "mail-message-new" + MailReplyAll = "mail-reply-all" + MailReplySender = "mail-reply-sender" + MailSend = "mail-send" + MediaEject = "media-eject" + MediaPlaybackPause = "media-playback-pause" + MediaPlaybackStart = "media-playback-start" + MediaPlaybackStop = "media-playback-stop" + MediaRecord = "media-record" + MediaSeekBackward = "media-seek-backward" + MediaSeekForward = "media-seek-forward" + MediaSkipBackward = "media-skip-backward" + MediaSkipForward = "media-skip-forward" + ObjectRotateLeft = "object-rotate-left" + ObjectRotateRight = "object-rotate-right" + ProcessStop = "process-stop" + SystemLockScreen = "system-lock-screen" + SystemLogOut = "system-log-out" + SystemSearch = "system-search" + SystemReboot = "system-reboot" + SystemShutdown = "system-shutdown" + ToolsCheckSpelling = "tools-check-spelling" + ViewFullscreen = "view-fullscreen" + ViewRefresh = "view-refresh" + ViewRestore = "view-restore" + WindowClose = "window-close" + WindowNew = "window-new" + ZoomFitBest = "zoom-fit-best" + ZoomIn = "zoom-in" + ZoomOut = "zoom-out" + AudioCard = "audio-card" + AudioInputMicrophone = "audio-input-microphone" + Battery = "battery" + CameraPhoto = "camera-photo" + CameraVideo = "camera-video" + CameraWeb = "camera-web" + Computer = "computer" + DriveHarddisk = "drive-harddisk" + DriveOptical = "drive-optical" + InputGaming = "input-gaming" + InputKeyboard = "input-keyboard" + InputMouse = "input-mouse" + InputTablet = "input-tablet" + MediaFlash = "media-flash" + MediaOptical = "media-optical" + MediaTape = "media-tape" + MultimediaPlayer = "multimedia-player" + NetworkWired = "network-wired" + NetworkWireless = "network-wireless" + Phone = "phone" + Printer = "printer" + Scanner = "scanner" + VideoDisplay = "video-display" + AppointmentMissed = "appointment-missed" + AppointmentSoon = "appointment-soon" + AudioVolumeHigh = "audio-volume-high" + AudioVolumeLow = "audio-volume-low" + AudioVolumeMedium = "audio-volume-medium" + AudioVolumeMuted = "audio-volume-muted" + BatteryCaution = "battery-caution" + BatteryLow = "battery-low" + DialogError = "dialog-error" + DialogInformation = "dialog-information" + DialogPassword = "dialog-password" + DialogQuestion = "dialog-question" + DialogWarning = "dialog-warning" + FolderDragAccept = "folder-drag-accept" + FolderOpen = "folder-open" + FolderVisiting = "folder-visiting" + ImageLoading = "image-loading" + ImageMissing = "image-missing" + MailAttachment = "mail-attachment" + MailUnread = "mail-unread" + MailRead = "mail-read" + MailReplied = "mail-replied" + MediaPlaylistRepeat = "media-playlist-repeat" + MediaPlaylistShuffle = "media-playlist-shuffle" + NetworkOffline = "network-offline" + PrinterPrinting = "printer-printing" + SecurityHigh = "security-high" + SecurityLow = "security-low" + SoftwareUpdateAvailable = "software-update-available" + SoftwareUpdateUrgent = "software-update-urgent" + SyncError = "sync-error" + SyncSynchronizing = "sync-synchronizing" + UserAvailable = "user-available" + UserOffline = "user-offline" + WeatherClear = "weather-clear" + WeatherClearNight = "weather-clear-night" + WeatherFewClouds = "weather-few-clouds" + WeatherFewCloudsNight = "weather-few-clouds-night" + WeatherFog = "weather-fog" + WeatherShowers = "weather-showers" + WeatherSnow = "weather-snow" + WeatherStorm = "weather-storm" + + QIcon.ThemeIcon = _ThemeIcon diff --git a/qtpy/QtHelp.py b/qtpy/QtHelp.py new file mode 100644 index 00000000..ee6df4c8 --- /dev/null +++ b/qtpy/QtHelp.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""QtHelp Wrapper.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtHelp import * +elif PYQT6: + from PyQt6 import QtHelp + from PyQt6.QtHelp import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtHelp) + del QtHelp +elif PYSIDE2: + from PySide2.QtHelp import * +elif PYSIDE6: + from PySide6.QtHelp import * diff --git a/qtpy/QtLocation.py b/qtpy/QtLocation.py new file mode 100644 index 00000000..160c9476 --- /dev/null +++ b/qtpy/QtLocation.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtLocation classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.QtLocation import * +elif PYQT6: + raise QtBindingMissingModuleError(name="QtLocation") +elif PYSIDE2: + from PySide2.QtLocation import * +elif PYSIDE6: + raise QtBindingMissingModuleError(name="QtLocation") diff --git a/qtpy/QtMacExtras.py b/qtpy/QtMacExtras.py new file mode 100644 index 00000000..58d23b8c --- /dev/null +++ b/qtpy/QtMacExtras.py @@ -0,0 +1,31 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides classes and functions specific to macOS and iOS operating systems""" + +import sys + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInOSError, + QtModuleNotInQtVersionError, +) + +if sys.platform == "darwin": + if PYQT5: + from PyQt5.QtMacExtras import * + elif PYQT6: + raise QtModuleNotInQtVersionError(name="QtMacExtras") + elif PYSIDE2: + from PySide2.QtMacExtras import * + elif PYSIDE6: + raise QtModuleNotInQtVersionError(name="QtMacExtras") +else: + raise QtModuleNotInOSError(name="QtMacExtras") diff --git a/qtpy/QtMultimedia.py b/qtpy/QtMultimedia.py index ce3c929e..80a9cb17 100644 --- a/qtpy/QtMultimedia.py +++ b/qtpy/QtMultimedia.py @@ -1,13 +1,26 @@ -from qtpy import PYQT5 -from qtpy import PYQT4 -from qtpy import PYSIDE +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- +"""Provides low-level multimedia functionality.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 if PYQT5: from PyQt5.QtMultimedia import * -elif PYQT4: - from PyQt4.QtMultimedia import * - from PyQt4.QtGui import QSound -elif PYSIDE: - from PySide.QtMultimedia import * - from PySide.QtGui import QSound +elif PYQT6: + from PyQt6 import QtMultimedia + from PyQt6.QtMultimedia import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtMultimedia) + del QtMultimedia +elif PYSIDE2: + from PySide2.QtMultimedia import * +elif PYSIDE6: + from PySide6.QtMultimedia import * diff --git a/qtpy/QtMultimediaWidgets.py b/qtpy/QtMultimediaWidgets.py new file mode 100644 index 00000000..69af111a --- /dev/null +++ b/qtpy/QtMultimediaWidgets.py @@ -0,0 +1,19 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtMultimediaWidgets classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtMultimediaWidgets import * +elif PYQT6: + from PyQt6.QtMultimediaWidgets import * +elif PYSIDE2: + from PySide2.QtMultimediaWidgets import * +elif PYSIDE6: + from PySide6.QtMultimediaWidgets import * diff --git a/qtpy/QtNetwork.py b/qtpy/QtNetwork.py index aca335bd..a4b9c929 100644 --- a/qtpy/QtNetwork.py +++ b/qtpy/QtNetwork.py @@ -1,23 +1,27 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy # Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- -""" -Provides QtNetwork classes and functions. -""" - -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError +"""Provides QtNetwork classes and functions.""" +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 if PYQT5: from PyQt5.QtNetwork import * -elif PYQT4: - from PyQt4.QtNetwork import * -elif PYSIDE: - from PySide.QtNetwork import * -else: - raise PythonQtError('No Qt bindings could be found') +elif PYQT6: + from PyQt6 import QtNetwork + from PyQt6.QtNetwork import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtNetwork) + del QtNetwork +elif PYSIDE2: + from PySide2.QtNetwork import * +elif PYSIDE6: + from PySide6.QtNetwork import * diff --git a/qtpy/QtNetworkAuth.py b/qtpy/QtNetworkAuth.py new file mode 100644 index 00000000..f49adcec --- /dev/null +++ b/qtpy/QtNetworkAuth.py @@ -0,0 +1,38 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtNetworkAuth classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.QtNetworkAuth import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtNetworkAuth", + missing_package="PyQtNetworkAuth", + ) from error +elif PYQT6: + try: + from PyQt6.QtNetworkAuth import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtNetworkAuth", + missing_package="PyQt6-NetworkAuth", + ) from error +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtNetworkAuth") +elif PYSIDE6: + from PySide6.QtNetworkAuth import * diff --git a/qtpy/QtNfc.py b/qtpy/QtNfc.py new file mode 100644 index 00000000..8dafd7ce --- /dev/null +++ b/qtpy/QtNfc.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtNfc classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.QtNfc import * +elif PYQT6: + from PyQt6.QtNfc import * +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtNfc") +elif PYSIDE6: + from PySide6.QtNfc import * diff --git a/qtpy/QtOpenGL.py b/qtpy/QtOpenGL.py new file mode 100644 index 00000000..071e87cf --- /dev/null +++ b/qtpy/QtOpenGL.py @@ -0,0 +1,73 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtOpenGL classes and functions.""" + +import contextlib + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtGui import ( + QOpenGLBuffer, + QOpenGLContext, + QOpenGLContextGroup, + QOpenGLDebugLogger, + QOpenGLDebugMessage, + QOpenGLFramebufferObject, + QOpenGLFramebufferObjectFormat, + QOpenGLPixelTransferOptions, + QOpenGLShader, + QOpenGLShaderProgram, + QOpenGLTexture, + QOpenGLTextureBlitter, + QOpenGLVersionProfile, + QOpenGLVertexArrayObject, + QOpenGLWindow, + ) + from PyQt5.QtOpenGL import * + + # These are not present on some architectures such as armhf + with contextlib.suppress(ImportError): + from PyQt5.QtGui import QOpenGLTimeMonitor, QOpenGLTimerQuery + +elif PYQT6: + from PyQt6 import QtOpenGL + from PyQt6.QtGui import QOpenGLContext, QOpenGLContextGroup + from PyQt6.QtOpenGL import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtOpenGL) + del QtOpenGL +elif PYSIDE6: + from PySide6.QtGui import QOpenGLContext, QOpenGLContextGroup + from PySide6.QtOpenGL import * +elif PYSIDE2: + from PySide2.QtGui import ( + QOpenGLBuffer, + QOpenGLContext, + QOpenGLContextGroup, + QOpenGLDebugLogger, + QOpenGLDebugMessage, + QOpenGLFramebufferObject, + QOpenGLFramebufferObjectFormat, + QOpenGLPixelTransferOptions, + QOpenGLShader, + QOpenGLShaderProgram, + QOpenGLTexture, + QOpenGLTextureBlitter, + QOpenGLVersionProfile, + QOpenGLVertexArrayObject, + QOpenGLWindow, + ) + from PySide2.QtOpenGL import * + + # These are not present on some architectures such as armhf + with contextlib.suppress(ImportError): + from PySide2.QtGui import QOpenGLTimeMonitor, QOpenGLTimerQuery diff --git a/qtpy/QtOpenGLWidgets.py b/qtpy/QtOpenGLWidgets.py new file mode 100644 index 00000000..02db86a4 --- /dev/null +++ b/qtpy/QtOpenGLWidgets.py @@ -0,0 +1,32 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtOpenGLWidgets classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + raise QtBindingMissingModuleError(name="QtOpenGLWidgets") +elif PYQT6: + from PyQt6 import QtOpenGLWidgets + from PyQt6.QtOpenGLWidgets import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtOpenGLWidgets) + del QtOpenGLWidgets +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtOpenGLWidgets") +elif PYSIDE6: + from PySide6.QtOpenGLWidgets import * diff --git a/qtpy/QtPdf.py b/qtpy/QtPdf.py new file mode 100644 index 00000000..f98cbd0f --- /dev/null +++ b/qtpy/QtPdf.py @@ -0,0 +1,27 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtPdf classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + raise QtBindingMissingModuleError(name="QtPdf") +elif PYQT6: + # Available with version >=6.4.0 + from PyQt6.QtPdf import * +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtPdf") +elif PYSIDE6: + # Available with version >=6.4.0 + from PySide6.QtPdf import * diff --git a/qtpy/QtPdfWidgets.py b/qtpy/QtPdfWidgets.py new file mode 100644 index 00000000..a437db60 --- /dev/null +++ b/qtpy/QtPdfWidgets.py @@ -0,0 +1,27 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtPdfWidgets classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + raise QtBindingMissingModuleError(name="QtPdfWidgets") +elif PYQT6: + # Available with version >=6.4.0 + from PyQt6.QtPdfWidgets import * +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtPdfWidgets") +elif PYSIDE6: + # Available with version >=6.4.0 + from PySide6.QtPdfWidgets import * diff --git a/qtpy/QtPositioning.py b/qtpy/QtPositioning.py new file mode 100644 index 00000000..508d5607 --- /dev/null +++ b/qtpy/QtPositioning.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright 2020 Antonio Valentino +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtPositioning classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtPositioning import * +elif PYQT6: + from PyQt6 import QtPositioning + from PyQt6.QtPositioning import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtPositioning) + del QtPositioning +elif PYSIDE2: + from PySide2.QtPositioning import * +elif PYSIDE6: + from PySide6.QtPositioning import * diff --git a/qtpy/QtPrintSupport.py b/qtpy/QtPrintSupport.py index b272174b..0b3725de 100644 --- a/qtpy/QtPrintSupport.py +++ b/qtpy/QtPrintSupport.py @@ -1,26 +1,49 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- -""" -Provides QtPrintSupport classes and functions. -""" - -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError +"""Provides QtPrintSupport classes and functions.""" +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 if PYQT5: from PyQt5.QtPrintSupport import * -elif PYQT4: - from PyQt4.QtGui import (QAbstractPrintDialog, QPageSetupDialog, - QPrintDialog, QPrintEngine, QPrintPreviewDialog, - QPrintPreviewWidget, QPrinter, QPrinterInfo) -elif PYSIDE: - from PySide.QtGui import (QAbstractPrintDialog, QPageSetupDialog, - QPrintDialog, QPrintEngine, QPrintPreviewDialog, - QPrintPreviewWidget, QPrinter, QPrinterInfo) -else: - raise PythonQtError('No Qt bindings could be found') +elif PYQT6: + from PyQt6 import QtPrintSupport + from PyQt6.QtPrintSupport import * + + QPageSetupDialog.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) + QPrintDialog.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) + QPrintPreviewWidget.print_ = lambda self, *args, **kwargs: self.print( + *args, + **kwargs, + ) + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtPrintSupport) + del QtPrintSupport +elif PYSIDE6: + from PySide6.QtPrintSupport import * + + # Map DeprecationWarning methods + QPageSetupDialog.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) + QPrintDialog.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) +elif PYSIDE2: + from PySide2.QtPrintSupport import * diff --git a/qtpy/QtPurchasing.py b/qtpy/QtPurchasing.py new file mode 100644 index 00000000..fd694483 --- /dev/null +++ b/qtpy/QtPurchasing.py @@ -0,0 +1,28 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtPurchasing classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.QtPurchasing import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtPurchasing", + missing_package="PyQtPurchasing", + ) from error +elif PYQT6 or PYSIDE2 or PYSIDE6: + raise QtBindingMissingModuleError(name="QtPurchasing") diff --git a/qtpy/QtQml.py b/qtpy/QtQml.py new file mode 100644 index 00000000..1a229813 --- /dev/null +++ b/qtpy/QtQml.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtQml classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtQml import * +elif PYQT6: + from PyQt6 import QtQml + from PyQt6.QtQml import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtQml) + del QtQml +elif PYSIDE6: + from PySide6.QtQml import * +elif PYSIDE2: + from PySide2.QtQml import * diff --git a/qtpy/QtQuick.py b/qtpy/QtQuick.py new file mode 100644 index 00000000..39dc5fb2 --- /dev/null +++ b/qtpy/QtQuick.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtQuick classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtQuick import * +elif PYQT6: + from PyQt6 import QtQuick + from PyQt6.QtQuick import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtQuick) + del QtQuick +elif PYSIDE6: + from PySide6.QtQuick import * +elif PYSIDE2: + from PySide2.QtQuick import * diff --git a/qtpy/QtQuick3D.py b/qtpy/QtQuick3D.py new file mode 100644 index 00000000..a8138f97 --- /dev/null +++ b/qtpy/QtQuick3D.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtQuick3D classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.QtQuick3D import * +elif PYQT6: + from PyQt6.QtQuick3D import * +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtQuick3D") +elif PYSIDE6: + from PySide6.QtQuick3D import * diff --git a/qtpy/QtQuickControls2.py b/qtpy/QtQuickControls2.py new file mode 100644 index 00000000..634d5445 --- /dev/null +++ b/qtpy/QtQuickControls2.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtQuickControls2 classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5 or PYQT6: + raise QtBindingMissingModuleError(name="QtQuickControls2") +elif PYSIDE2: + from PySide2.QtQuickControls2 import * +elif PYSIDE6: + from PySide6.QtQuickControls2 import * diff --git a/qtpy/QtQuickWidgets.py b/qtpy/QtQuickWidgets.py new file mode 100644 index 00000000..cc5d0df3 --- /dev/null +++ b/qtpy/QtQuickWidgets.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtQuickWidgets classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtQuickWidgets import * +elif PYQT6: + from PyQt6 import QtQuickWidgets + from PyQt6.QtQuickWidgets import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtQuickWidgets) + del QtQuickWidgets +elif PYSIDE6: + from PySide6.QtQuickWidgets import * +elif PYSIDE2: + from PySide2.QtQuickWidgets import * diff --git a/qtpy/QtRemoteObjects.py b/qtpy/QtRemoteObjects.py new file mode 100644 index 00000000..1035586b --- /dev/null +++ b/qtpy/QtRemoteObjects.py @@ -0,0 +1,19 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtRemoteObjects classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtRemoteObjects import * +elif PYQT6: + from PyQt6.QtRemoteObjects import * +elif PYSIDE6: + from PySide6.QtRemoteObjects import * +elif PYSIDE2: + from PySide2.QtRemoteObjects import * diff --git a/qtpy/QtScxml.py b/qtpy/QtScxml.py new file mode 100644 index 00000000..40da5ef7 --- /dev/null +++ b/qtpy/QtScxml.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtScxml classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5 or PYQT6: + raise QtBindingMissingModuleError(name="QtScxml") +elif PYSIDE2: + from PySide2.QtScxml import * +elif PYSIDE6: + from PySide6.QtScxml import * diff --git a/qtpy/QtSensors.py b/qtpy/QtSensors.py new file mode 100644 index 00000000..472cb30c --- /dev/null +++ b/qtpy/QtSensors.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtSensors classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtSensors import * +elif PYQT6: + from PyQt6 import QtSensors + from PyQt6.QtSensors import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtSensors) + del QtSensors +elif PYSIDE6: + from PySide6.QtSensors import * +elif PYSIDE2: + from PySide2.QtSensors import * diff --git a/qtpy/QtSerialPort.py b/qtpy/QtSerialPort.py new file mode 100644 index 00000000..878c35b1 --- /dev/null +++ b/qtpy/QtSerialPort.py @@ -0,0 +1,20 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2020 Marcin Stano +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtSerialPort classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtSerialPort import * +elif PYQT6: + from PyQt6.QtSerialPort import * +elif PYSIDE6: + from PySide6.QtSerialPort import * +elif PYSIDE2: + from PySide2.QtSerialPort import * diff --git a/qtpy/QtSql.py b/qtpy/QtSql.py new file mode 100644 index 00000000..dd257637 --- /dev/null +++ b/qtpy/QtSql.py @@ -0,0 +1,41 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtSql classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtSql import * +elif PYQT6: + from PyQt6 import QtSql + from PyQt6.QtSql import * + + QSqlDatabase.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) + QSqlQuery.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QSqlResult.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtSql) + del QtSql +elif PYSIDE6: + from PySide6.QtSql import * + + # Map DeprecationWarning methods + QSqlDatabase.exec_ = lambda self, *args, **kwargs: self.exec( + *args, + **kwargs, + ) + QSqlQuery.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QSqlResult.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) +elif PYSIDE2: + from PySide2.QtSql import * diff --git a/qtpy/QtStateMachine.py b/qtpy/QtStateMachine.py new file mode 100644 index 00000000..d2cf2546 --- /dev/null +++ b/qtpy/QtStateMachine.py @@ -0,0 +1,64 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtStateMachine classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, +) + +if PYQT5: + from PyQt5.QtCore import ( + QAbstractState, + QAbstractTransition, + QEventTransition, + QFinalState, + QHistoryState, + QSignalTransition, + QState, + QStateMachine, + ) + from PyQt5.QtWidgets import ( + QKeyEventTransition, + QMouseEventTransition, + ) +elif PYSIDE2: + from PySide2.QtCore import ( + QAbstractState, + QAbstractTransition, + QEventTransition, + QFinalState, + QHistoryState, + QSignalTransition, + QState, + QStateMachine, + ) + from PySide2.QtWidgets import ( + QKeyEventTransition, + QMouseEventTransition, + ) +elif PYQT6: + from PyQt6.QtCore import PYQT_VERSION_STR + + if int(PYQT_VERSION_STR.split(".")[1]) >= 9: + from PyQt6 import QtStateMachine + from PyQt6.QtStateMachine import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtStateMachine) + del QtStateMachine + else: + from . import QtBindingInNewerVersionError + + raise QtBindingInNewerVersionError(name="QtStateMachine") +elif PYSIDE6: + from PySide6.QtStateMachine import * diff --git a/qtpy/QtSvg.py b/qtpy/QtSvg.py index 2f210ca8..0ee4f9e1 100644 --- a/qtpy/QtSvg.py +++ b/qtpy/QtSvg.py @@ -1,25 +1,19 @@ -# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) # ----------------------------------------------------------------------------- + """Provides QtSvg classes and functions.""" -# Local imports -from qtpy import PYQT4, PYQT5, PYSIDE, PythonQtError +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 if PYQT5: - from PyQt5.QtSvg import (QGraphicsSvgItem, QSvgGenerator, QSvgRenderer, - QSvgWidget) -elif PYQT4: - from PyQt4.QtSvg import (QGraphicsSvgItem, QSvgGenerator, QSvgRenderer, - QSvgWidget) -elif PYSIDE: - from PySide.QtSvg import (QGraphicsSvgItem, QSvgGenerator, QSvgRenderer, - QSvgWidget) -else: - raise PythonQtError('No Qt bindings could be found') - -del PYQT4, PYQT5, PYSIDE + from PyQt5.QtSvg import * +elif PYQT6: + from PyQt6.QtSvg import * +elif PYSIDE2: + from PySide2.QtSvg import * +elif PYSIDE6: + from PySide6.QtSvg import * diff --git a/qtpy/QtSvgWidgets.py b/qtpy/QtSvgWidgets.py new file mode 100644 index 00000000..7e91dfc9 --- /dev/null +++ b/qtpy/QtSvgWidgets.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtSvgWidgets classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + raise QtBindingMissingModuleError(name="QtSvgWidgets") +elif PYQT6: + from PyQt6.QtSvgWidgets import * +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtSvgWidgets") +elif PYSIDE6: + from PySide6.QtSvgWidgets import * diff --git a/qtpy/QtTest.py b/qtpy/QtTest.py index 6d59c765..b14418f2 100644 --- a/qtpy/QtTest.py +++ b/qtpy/QtTest.py @@ -1,28 +1,27 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy -# Copyright © 2009- The Spyder Developmet Team +# Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- -""" -Provides QtTest and functions -""" - -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError +"""Provides QtTest and functions""" +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 if PYQT5: - from PyQt5.QtTest import QTest -elif PYQT4: - from PyQt4.QtTest import QTest as OldQTest + from PyQt5.QtTest import * +elif PYQT6: + from PyQt6 import QtTest + from PyQt6.QtTest import * + + # Allow unscoped access for enums inside the QtTest module + from .enums_compat import promote_enums - class QTest(OldQTest): - @staticmethod - def qWaitForWindowActive(QWidget): - OldQTest.qWaitForWindowShown(QWidget) -elif PYSIDE: - from PySide.QtTest import QTest -else: - raise PythonQtError('No Qt bindings could be found') + promote_enums(QtTest) + del QtTest +elif PYSIDE2: + from PySide2.QtTest import * +elif PYSIDE6: + from PySide6.QtTest import * diff --git a/qtpy/QtTextToSpeech.py b/qtpy/QtTextToSpeech.py new file mode 100644 index 00000000..c37192a1 --- /dev/null +++ b/qtpy/QtTextToSpeech.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtTextToSpeech classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.QtTextToSpeech import * +elif PYQT6: + raise QtBindingMissingModuleError(name="QtTextToSpeech") +elif PYSIDE2: + from PySide2.QtTextToSpeech import * +elif PYSIDE6: + raise QtBindingMissingModuleError(name="QtTextToSpeech") diff --git a/qtpy/QtUiTools.py b/qtpy/QtUiTools.py new file mode 100644 index 00000000..ceca1efa --- /dev/null +++ b/qtpy/QtUiTools.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtUiTools classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5 or PYQT6: + raise QtBindingMissingModuleError(name="QtUiTools") +elif PYSIDE2: + from PySide2.QtUiTools import * +elif PYSIDE6: + from PySide6.QtUiTools import * diff --git a/qtpy/QtWebChannel.py b/qtpy/QtWebChannel.py new file mode 100644 index 00000000..b2c35fff --- /dev/null +++ b/qtpy/QtWebChannel.py @@ -0,0 +1,19 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtWebChannel classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtWebChannel import * +elif PYQT6: + from PyQt6.QtWebChannel import * +elif PYSIDE2: + from PySide2.QtWebChannel import * +elif PYSIDE6: + from PySide6.QtWebChannel import * diff --git a/qtpy/QtWebEngine.py b/qtpy/QtWebEngine.py new file mode 100644 index 00000000..7cc08ff0 --- /dev/null +++ b/qtpy/QtWebEngine.py @@ -0,0 +1,33 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2014-2015 Colin Duquesnoy +# Copyright © 2009- The Spyder development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtWebEngine classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInQtVersionError, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.QtWebEngine import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtWebEngine", + missing_package="PyQtWebEngine", + ) from error +elif PYQT6: + raise QtModuleNotInQtVersionError(name="QtWebEngine") +elif PYSIDE2: + from PySide2.QtWebEngine import * +elif PYSIDE6: + raise QtModuleNotInQtVersionError(name="QtWebEngine") diff --git a/qtpy/QtWebEngineCore.py b/qtpy/QtWebEngineCore.py new file mode 100644 index 00000000..62d532d1 --- /dev/null +++ b/qtpy/QtWebEngineCore.py @@ -0,0 +1,44 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtWebEngineCore classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) + +if PYQT5: + try: + from PyQt5.QtWebEngineCore import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtWebEngineCore", + missing_package="PyQtWebEngine", + ) from error +elif PYQT6: + try: + from PyQt6 import QtWebEngineCore + from PyQt6.QtWebEngineCore import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtWebEngineCore) + del QtWebEngineCore + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtWebEngineCore", + missing_package="PyQt6-WebEngine", + ) from error +elif PYSIDE2: + from PySide2.QtWebEngineCore import * +elif PYSIDE6: + from PySide6.QtWebEngineCore import * diff --git a/qtpy/QtWebEngineQuick.py b/qtpy/QtWebEngineQuick.py new file mode 100644 index 00000000..4798045b --- /dev/null +++ b/qtpy/QtWebEngineQuick.py @@ -0,0 +1,39 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtWebEngineQuick classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, + QtModuleNotInstalledError, +) + +if PYQT5: + raise QtBindingMissingModuleError(name="QtWebEngineQuick") +elif PYQT6: + try: + from PyQt6 import QtWebEngineQuick + from PyQt6.QtWebEngineQuick import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtWebEngineQuick) + del QtWebEngineQuick + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtWebEngineQuick", + missing_package="PyQt6-WebEngine", + ) from error +elif PYSIDE2: + raise QtBindingMissingModuleError(name="QtWebEngineQuick") +elif PYSIDE6: + from PySide6.QtWebEngineQuick import * diff --git a/qtpy/QtWebEngineWidgets.py b/qtpy/QtWebEngineWidgets.py index bd073691..a39885f3 100644 --- a/qtpy/QtWebEngineWidgets.py +++ b/qtpy/QtWebEngineWidgets.py @@ -1,41 +1,70 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy # Copyright © 2009- The Spyder development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- -""" -Provides QtWebEngineWidgets classes and functions. -""" - -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError +"""Provides QtWebEngineWidgets classes and functions.""" +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInstalledError, +) # To test if we are using WebEngine or WebKit +# NOTE: This constant is imported by other projects (e.g. Spyder), so please +# don't remove it. WEBENGINE = True if PYQT5: try: - from PyQt5.QtWebEngineWidgets import QWebEnginePage - from PyQt5.QtWebEngineWidgets import QWebEngineView - from PyQt5.QtWebEngineWidgets import QWebEngineSettings - except ImportError: - from PyQt5.QtWebKitWidgets import QWebPage as QWebEnginePage - from PyQt5.QtWebKitWidgets import QWebView as QWebEngineView - from PyQt5.QtWebKit import QWebSettings as QWebEngineSettings - WEBENGINE = False -elif PYQT4: - from PyQt4.QtWebKit import QWebPage as QWebEnginePage - from PyQt4.QtWebKit import QWebView as QWebEngineView - from PyQt4.QtWebKit import QWebSettings as QWebEngineSettings - WEBENGINE = False -elif PYSIDE: - from PySide.QtWebKit import QWebPage as QWebEnginePage - from PySide.QtWebKit import QWebView as QWebEngineView - from PySide.QtWebKit import QWebSettings as QWebEngineSettings - WEBENGINE = False -else: - raise PythonQtError('No Qt bindings could be found') + # Based on the work at https://github.com/spyder-ide/qtpy/pull/203 + from PyQt5.QtWebEngineWidgets import ( + QWebEnginePage, + QWebEngineProfile, + QWebEngineScript, + QWebEngineSettings, + QWebEngineView, + ) + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtWebEngineWidgets", + missing_package="PyQtWebEngine", + ) from error +elif PYQT6: + try: + from PyQt6.QtWebEngineCore import ( + QWebEnginePage, + QWebEngineProfile, + QWebEngineScript, + QWebEngineSettings, + ) + from PyQt6.QtWebEngineWidgets import * + except ModuleNotFoundError as error: + raise QtModuleNotInstalledError( + name="QtWebEngineWidgets", + missing_package="PyQt6-WebEngine", + ) from error +elif PYSIDE2: + # Based on the work at https://github.com/spyder-ide/qtpy/pull/203 + from PySide2.QtWebEngineWidgets import ( + QWebEnginePage, + QWebEngineProfile, + QWebEngineScript, + QWebEngineSettings, + QWebEngineView, + ) +elif PYSIDE6: + from PySide6.QtWebEngineCore import ( + QWebEnginePage, + QWebEngineProfile, + QWebEngineScript, + QWebEngineSettings, + ) + from PySide6.QtWebEngineWidgets import * diff --git a/qtpy/QtWebSockets.py b/qtpy/QtWebSockets.py new file mode 100644 index 00000000..a9bd33d9 --- /dev/null +++ b/qtpy/QtWebSockets.py @@ -0,0 +1,19 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtWebSockets classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtWebSockets import * +elif PYQT6: + from PyQt6.QtWebSockets import * +elif PYSIDE2: + from PySide2.QtWebSockets import * +elif PYSIDE6: + from PySide6.QtWebSockets import * diff --git a/qtpy/QtWidgets.py b/qtpy/QtWidgets.py index 267d16b7..4354bc8e 100644 --- a/qtpy/QtWidgets.py +++ b/qtpy/QtWidgets.py @@ -1,122 +1,238 @@ -# -*- coding: utf-8 -*- -# +# ----------------------------------------------------------------------------- # Copyright © 2014-2015 Colin Duquesnoy -# Copyright © 2009- The Spyder Developmet Team +# Copyright © 2009- The Spyder Development Team # # Licensed under the terms of the MIT License # (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides widget classes and functions.""" +from functools import partialmethod + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, _parse_version +from . import QT_VERSION as _qt_version +from ._utils import ( + add_action, + getattr_missing_optional_dep, + possibly_static_exec, + static_method_kwargs_wrapper, +) -""" -Provides widget classes and functions. -.. warning:: Only PyQt4/PySide QtGui classes compatible with PyQt5.QtWidgets - are exposed here. Therefore, you need to treat/use this package as if it - were the ``PyQt5.QtWidgets`` module. -""" +_missing_optional_names = {} -from qtpy import PYQT5, PYQT4, PYSIDE, PythonQtError -from qtpy._patch.qcombobox import patch_qcombobox -from qtpy._patch.qheaderview import introduce_renamed_methods_qheaderview + +def __getattr__(name): + """Custom getattr to chain and wrap errors due to missing optional deps.""" + raise getattr_missing_optional_dep( + name, + module_name=__name__, + optional_names=_missing_optional_names, + ) if PYQT5: from PyQt5.QtWidgets import * -elif PYQT4: - from PyQt4.QtGui import * - QStyleOptionViewItem = QStyleOptionViewItemV4 - del QStyleOptionViewItemV4 - - # These objects belong to QtGui - del (QAbstractTextDocumentLayout, QActionEvent, QBitmap, QBrush, QClipboard, - QCloseEvent, QColor, QConicalGradient, QContextMenuEvent, QCursor, - QDesktopServices, QDoubleValidator, QDrag, QDragEnterEvent, - QDragLeaveEvent, QDragMoveEvent, QDropEvent, QFileOpenEvent, - QFocusEvent, QFont, QFontDatabase, QFontInfo, QFontMetrics, - QFontMetricsF, QGlyphRun, QGradient, QHelpEvent, QHideEvent, - QHoverEvent, QIcon, QIconDragEvent, QIconEngine, QImage, - QImageIOHandler, QImageReader, QImageWriter, QInputEvent, - QInputMethodEvent, QKeyEvent, QKeySequence, QLinearGradient, - QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, QMatrix3x3, - QMatrix3x4, QMatrix4x2, QMatrix4x3, QMatrix4x4, QMouseEvent, - QMoveEvent, QMovie, QPaintDevice, QPaintEngine, QPaintEngineState, - QPaintEvent, QPainter, QPainterPath, QPainterPathStroker, QPalette, - QPen, QPicture, QPictureIO, QPixmap, QPixmapCache, QPolygon, - QPolygonF, QQuaternion, QRadialGradient, QRawFont, QRegExpValidator, - QRegion, QResizeEvent, QSessionManager, QShortcutEvent, QShowEvent, - QStandardItem, QStandardItemModel, QStaticText, QStatusTipEvent, - QSyntaxHighlighter, QTabletEvent, QTextBlock, QTextBlockFormat, - QTextBlockGroup, QTextBlockUserData, QTextCharFormat, QTextCursor, - QTextDocument, QTextDocumentFragment, QTextDocumentWriter, - QTextFormat, QTextFragment, QTextFrame, QTextFrameFormat, - QTextImageFormat, QTextInlineObject, QTextItem, QTextLayout, - QTextLength, QTextLine, QTextList, QTextListFormat, QTextObject, - QTextObjectInterface, QTextOption, QTextTable, QTextTableCell, - QTextTableCellFormat, QTextTableFormat, QTouchEvent, QTransform, - QValidator, QVector2D, QVector3D, QVector4D, QWhatsThisClickedEvent, - QWheelEvent, QWindowStateChangeEvent, qAlpha, qBlue, qFuzzyCompare, - qGray, qGreen, qIsGray, qRed, qRgb, qRgba, QIntValidator) - - # These objects belong to QtPrintSupport - del (QAbstractPrintDialog, QPageSetupDialog, QPrintDialog, QPrintEngine, - QPrintPreviewDialog, QPrintPreviewWidget, QPrinter, QPrinterInfo) - - # These objects belong to QtCore - del (QItemSelection, QItemSelectionModel, QItemSelectionRange, - QSortFilterProxyModel) - - # Patch QComboBox to allow Python objects to be passed to userData - patch_qcombobox(QComboBox) - - # QHeaderView: renamed methods - introduce_renamed_methods_qheaderview(QHeaderView) - -elif PYSIDE: - from PySide.QtGui import * - QStyleOptionViewItem = QStyleOptionViewItemV4 - del QStyleOptionViewItemV4 - - # These objects belong to QtGui - del (QAbstractTextDocumentLayout, QActionEvent, QBitmap, QBrush, QClipboard, - QCloseEvent, QColor, QConicalGradient, QContextMenuEvent, QCursor, - QDesktopServices, QDoubleValidator, QDrag, QDragEnterEvent, - QDragLeaveEvent, QDragMoveEvent, QDropEvent, QFileOpenEvent, - QFocusEvent, QFont, QFontDatabase, QFontInfo, QFontMetrics, - QFontMetricsF, QGradient, QHelpEvent, QHideEvent, - QHoverEvent, QIcon, QIconDragEvent, QIconEngine, QImage, - QImageIOHandler, QImageReader, QImageWriter, QInputEvent, - QInputMethodEvent, QKeyEvent, QKeySequence, QLinearGradient, - QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, QMatrix3x3, - QMatrix3x4, QMatrix4x2, QMatrix4x3, QMatrix4x4, QMouseEvent, - QMoveEvent, QMovie, QPaintDevice, QPaintEngine, QPaintEngineState, - QPaintEvent, QPainter, QPainterPath, QPainterPathStroker, QPalette, - QPen, QPicture, QPictureIO, QPixmap, QPixmapCache, QPolygon, - QPolygonF, QQuaternion, QRadialGradient, QRegExpValidator, - QRegion, QResizeEvent, QSessionManager, QShortcutEvent, QShowEvent, - QStandardItem, QStandardItemModel, QStatusTipEvent, - QSyntaxHighlighter, QTabletEvent, QTextBlock, QTextBlockFormat, - QTextBlockGroup, QTextBlockUserData, QTextCharFormat, QTextCursor, - QTextDocument, QTextDocumentFragment, - QTextFormat, QTextFragment, QTextFrame, QTextFrameFormat, - QTextImageFormat, QTextInlineObject, QTextItem, QTextLayout, - QTextLength, QTextLine, QTextList, QTextListFormat, QTextObject, - QTextObjectInterface, QTextOption, QTextTable, QTextTableCell, - QTextTableCellFormat, QTextTableFormat, QTouchEvent, QTransform, - QValidator, QVector2D, QVector3D, QVector4D, QWhatsThisClickedEvent, - QWheelEvent, QWindowStateChangeEvent, qAlpha, qBlue, qGray, qGreen, - qIsGray, qRed, qRgb, qRgba, QIntValidator) - - # These objects belong to QtPrintSupport - del (QAbstractPrintDialog, QPageSetupDialog, QPrintDialog, QPrintEngine, - QPrintPreviewDialog, QPrintPreviewWidget, QPrinter, QPrinterInfo) - - # These objects belong to QtCore - del (QItemSelection, QItemSelectionModel, QItemSelectionRange, - QSortFilterProxyModel) - - # Patch QComboBox to allow Python objects to be passed to userData - patch_qcombobox(QComboBox) - - # QHeaderView: renamed methods - introduce_renamed_methods_qheaderview(QHeaderView) - +elif PYQT6: + from PyQt6 import QtWidgets + from PyQt6.QtGui import ( + QAction, + QActionGroup, + QFileSystemModel, + QShortcut, + QUndoCommand, + QUndoStack, + ) + + if _parse_version(_qt_version) < _parse_version("6.4"): + # Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4 + # See spyder-ide/qtpy#461 + from qtpy.QtGui import QAction + else: + from PyQt6.QtGui import QAction + + from PyQt6.QtWidgets import * + + # Attempt to import QOpenGLWidget, but if that fails, + # don't raise an exception until the name is explicitly accessed. + # See https://github.com/spyder-ide/qtpy/pull/387/ + try: + from PyQt6.QtOpenGLWidgets import QOpenGLWidget + except ImportError as error: + _missing_optional_names["QOpenGLWidget"] = { + "name": "PyQt6.QtOpenGLWidgets", + "missing_package": "pyopengl", + "import_error": error, + } + + # Map missing/renamed methods + QTextEdit.setTabStopWidth = ( + lambda self, *args, **kwargs: self.setTabStopDistance(*args, **kwargs) + ) + QTextEdit.tabStopWidth = ( + lambda self, *args, **kwargs: self.tabStopDistance(*args, **kwargs) + ) + QTextEdit.print_ = lambda self, *args, **kwargs: self.print( + *args, + **kwargs, + ) + QPlainTextEdit.setTabStopWidth = ( + lambda self, *args, **kwargs: self.setTabStopDistance(*args, **kwargs) + ) + QPlainTextEdit.tabStopWidth = ( + lambda self, *args, **kwargs: self.tabStopDistance(*args, **kwargs) + ) + QPlainTextEdit.print_ = lambda self, *args, **kwargs: self.print( + *args, + **kwargs, + ) + QApplication.exec_ = lambda *args, **kwargs: possibly_static_exec( + QApplication, + *args, + **kwargs, + ) + QDialog.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QMenu.exec_ = lambda *args, **kwargs: possibly_static_exec( + QMenu, + *args, + **kwargs, + ) + QLineEdit.getTextMargins = lambda self: ( + self.textMargins().left(), + self.textMargins().top(), + self.textMargins().right(), + self.textMargins().bottom(), + ) + + # Add removed definition for `QFileDialog.Options` as an alias of `QFileDialog.Option` + # passing as default value 0 in the same way PySide6 6.5+ does. + # Note that for PyQt5 and PySide2 those definitions are two different classes + # (one is the flag definition and the other the enum definition) + QFileDialog.Options = lambda value=0: QFileDialog.Option(value) + + # Allow unscoped access for enums inside the QtWidgets module + from .enums_compat import promote_enums + + promote_enums(QtWidgets) + del QtWidgets +elif PYSIDE2: + from PySide2.QtWidgets import * +elif PYSIDE6: + from PySide6.QtGui import QActionGroup, QShortcut, QUndoCommand, QUndoStack + + if _parse_version(_qt_version) < _parse_version("6.4"): + # Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4 + # See spyder-ide/qtpy#461 + from qtpy.QtGui import QAction + else: + from PySide6.QtGui import QAction + + from PySide6.QtWidgets import * + + # Attempt to import QOpenGLWidget, but if that fails, + # don't raise an exception until the name is explicitly accessed. + # See https://github.com/spyder-ide/qtpy/pull/387/ + try: + from PySide6.QtOpenGLWidgets import QOpenGLWidget + except ImportError as error: + _missing_optional_names["QOpenGLWidget"] = { + "name": "PySide6.QtOpenGLWidgets", + "missing_package": "pyopengl", + "import_error": error, + } + + # Map missing/renamed methods + QTextEdit.setTabStopWidth = ( + lambda self, *args, **kwargs: self.setTabStopDistance(*args, **kwargs) + ) + QTextEdit.tabStopWidth = ( + lambda self, *args, **kwargs: self.tabStopDistance(*args, **kwargs) + ) + QPlainTextEdit.setTabStopWidth = ( + lambda self, *args, **kwargs: self.setTabStopDistance(*args, **kwargs) + ) + QPlainTextEdit.tabStopWidth = ( + lambda self, *args, **kwargs: self.tabStopDistance(*args, **kwargs) + ) + QLineEdit.getTextMargins = lambda self: ( + self.textMargins().left(), + self.textMargins().top(), + self.textMargins().right(), + self.textMargins().bottom(), + ) + + # Map DeprecationWarning methods + QApplication.exec_ = lambda *args, **kwargs: possibly_static_exec( + QApplication, + *args, + **kwargs, + ) + QDialog.exec_ = lambda self, *args, **kwargs: self.exec(*args, **kwargs) + QMenu.exec_ = lambda *args, **kwargs: possibly_static_exec( + QMenu, + *args, + **kwargs, + ) + + # Passing as default value 0 in the same way PySide6 < 6.3.2 does for the `QFileDialog.Options` definition. + if _parse_version(_qt_version) > _parse_version("6.3"): + QFileDialog.Options = lambda value=0: QFileDialog.Option(value) + + +if PYSIDE2 or PYSIDE6: + # Make PySide2/6 `QFileDialog` static methods accept the `directory` kwarg as `dir` + QFileDialog.getExistingDirectory = static_method_kwargs_wrapper( + QFileDialog.getExistingDirectory, + "directory", + "dir", + ) + QFileDialog.getOpenFileName = static_method_kwargs_wrapper( + QFileDialog.getOpenFileName, + "directory", + "dir", + ) + QFileDialog.getOpenFileNames = static_method_kwargs_wrapper( + QFileDialog.getOpenFileNames, + "directory", + "dir", + ) + QFileDialog.getSaveFileName = static_method_kwargs_wrapper( + QFileDialog.getSaveFileName, + "directory", + "dir", + ) else: - raise PythonQtError('No Qt bindings could be found') + # Make PyQt5/6 `QFileDialog` static methods accept the `dir` kwarg as `directory` + QFileDialog.getExistingDirectory = static_method_kwargs_wrapper( + QFileDialog.getExistingDirectory, + "dir", + "directory", + ) + QFileDialog.getOpenFileName = static_method_kwargs_wrapper( + QFileDialog.getOpenFileName, + "dir", + "directory", + ) + QFileDialog.getOpenFileNames = static_method_kwargs_wrapper( + QFileDialog.getOpenFileNames, + "dir", + "directory", + ) + QFileDialog.getSaveFileName = static_method_kwargs_wrapper( + QFileDialog.getSaveFileName, + "dir", + "directory", + ) + +if PYQT5 or PYSIDE2 or _parse_version(_qt_version) < _parse_version("6.4"): + # Make `addAction` compatible with Qt6 >= 6.4 + _menu_add_action = partialmethod( + add_action, + old_add_action=QMenu.addAction, + ) + QMenu.addAction = _menu_add_action + + _toolbar_add_action = partialmethod( + add_action, + old_add_action=QToolBar.addAction, + ) + QToolBar.addAction = _toolbar_add_action diff --git a/qtpy/QtWinExtras.py b/qtpy/QtWinExtras.py new file mode 100644 index 00000000..bf2fb785 --- /dev/null +++ b/qtpy/QtWinExtras.py @@ -0,0 +1,31 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Windows-specific utilities""" + +import sys + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInOSError, + QtModuleNotInQtVersionError, +) + +if sys.platform == "win32": + if PYQT5: + from PyQt5.QtWinExtras import * + elif PYQT6: + raise QtModuleNotInQtVersionError(name="QtWinExtras") + elif PYSIDE2: + from PySide2.QtWinExtras import * + elif PYSIDE6: + raise QtModuleNotInQtVersionError(name="QtWinExtras") +else: + raise QtModuleNotInOSError(name="QtWinExtras") diff --git a/qtpy/QtX11Extras.py b/qtpy/QtX11Extras.py new file mode 100644 index 00000000..016727fb --- /dev/null +++ b/qtpy/QtX11Extras.py @@ -0,0 +1,31 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides Linux-specific utilities""" + +import sys + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtModuleNotInOSError, + QtModuleNotInQtVersionError, +) + +if sys.platform == "linux": + if PYQT5: + from PyQt5.QtX11Extras import * + elif PYQT6: + raise QtModuleNotInQtVersionError(name="QtX11Extras") + elif PYSIDE2: + from PySide2.QtX11Extras import * + elif PYSIDE6: + raise QtModuleNotInQtVersionError(name="QtX11Extras") +else: + raise QtModuleNotInOSError(name="QtX11Extras") diff --git a/qtpy/QtXml.py b/qtpy/QtXml.py new file mode 100644 index 00000000..be370327 --- /dev/null +++ b/qtpy/QtXml.py @@ -0,0 +1,26 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtXml classes and functions.""" + +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 + +if PYQT5: + from PyQt5.QtXml import * +elif PYQT6: + from PyQt6 import QtXml + from PyQt6.QtXml import * + + # Allow unscoped access for enums + from .enums_compat import promote_enums + + promote_enums(QtXml) + del QtXml +elif PYSIDE2: + from PySide2.QtXml import * +elif PYSIDE6: + from PySide6.QtXml import * diff --git a/qtpy/QtXmlPatterns.py b/qtpy/QtXmlPatterns.py new file mode 100644 index 00000000..a7e0b738 --- /dev/null +++ b/qtpy/QtXmlPatterns.py @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides QtXmlPatterns classes and functions.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.QtXmlPatterns import * +elif PYQT6: + raise QtBindingMissingModuleError(name="QtXmlPatterns") +elif PYSIDE2: + from PySide2.QtXmlPatterns import * +elif PYSIDE6: + raise QtBindingMissingModuleError(name="QtXmlPatterns") diff --git a/qtpy/__init__.py b/qtpy/__init__.py index b8373439..a1c4cbae 100644 --- a/qtpy/__init__.py +++ b/qtpy/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright © 2009- The Spyder Development Team # Copyright © 2014-2015 Colin Duquesnoy @@ -8,14 +7,15 @@ """ **QtPy** is a shim over the various Python Qt bindings. It is used to write -Qt binding indenpendent libraries or applications. +Qt binding independent libraries or applications. -The shim will automatically select the first available API (PyQt5, PyQt4 and -finally PySide). +If one of the APIs has already been imported, then it will be used. -You can force the use of one specific bindings (e.g. if your application is -using one specific bindings and you need to use library that use QtPy) by -setting up the ``QT_API`` environment variable. +Otherwise, the shim will automatically select the first available API (PyQt5, PySide2, +PyQt6 and PySide6); in that case, you can force the use of one +specific bindings (e.g. if your application is using one specific bindings and +you need to use library that use QtPy) by setting up the ``QT_API`` environment +variable. PyQt5 ===== @@ -25,110 +25,364 @@ >>> from qtpy import QtGui, QtWidgets, QtCore >>> print(QtWidgets.QWidget) +PySide2 +====== -PyQt4 -===== - -Set the ``QT_API`` environment variable to 'pyqt' before importing any python -package:: +Set the QT_API environment variable to 'pyside2' before importing other +packages:: >>> import os - >>> os.environ['QT_API'] = 'pyqt' + >>> os.environ['QT_API'] = 'pyside2' >>> from qtpy import QtGui, QtWidgets, QtCore >>> print(QtWidgets.QWidget) -PySide -====== +PyQt6 +===== -Set the QT_API environment variable to 'pyside' before importing other -packages:: + >>> import os + >>> os.environ['QT_API'] = 'pyqt6' + >>> from qtpy import QtGui, QtWidgets, QtCore + >>> print(QtWidgets.QWidget) + +PySide6 +======= >>> import os - >>> os.environ['QT_API'] = 'pyside' + >>> os.environ['QT_API'] = 'pyside6' >>> from qtpy import QtGui, QtWidgets, QtCore >>> print(QtWidgets.QWidget) """ +import contextlib import os +import platform +import sys +import warnings # Version of QtPy -from qtpy._version import __version__ - -#: Qt API environment variable name -QT_API = 'QT_API' -#: names of the expected PyQt5 api -PYQT5_API = ['pyqt5'] -#: names of the expected PyQt4 api -PYQT4_API = [ - 'pyqt', # name used in IPython.qt - 'pyqt4' # pyqode.qt original name -] -#: names of the expected PySide api -PYSIDE_API = ['pyside'] - -os.environ.setdefault(QT_API, 'pyqt5') -API = os.environ[QT_API].lower() -assert API in (PYQT5_API + PYQT4_API + PYSIDE_API) +__version__ = "2.5.0.dev0" + + +class PythonQtError(RuntimeError): + """Generic error superclass for QtPy.""" + + +class PythonQtWarning(RuntimeWarning): + """Warning class for QtPy.""" + + +class PythonQtValueError(ValueError): + """Error raised if an invalid QT_API is specified.""" + + +class QtBindingsNotFoundError(PythonQtError, ImportError): + """Error raised if no bindings could be selected.""" + + _msg = "No Qt bindings could be found" + + def __init__(self): + super().__init__(self._msg) + + +class QtModuleNotFoundError(ModuleNotFoundError, PythonQtError): + """Raised when a Python Qt binding submodule is not installed/supported.""" + + _msg = "The {name} module was not found." + _msg_binding = "{binding}" + _msg_extra = "" + + def __init__(self, *, name, msg=None, **msg_kwargs): + global API_NAME + binding = self._msg_binding.format(binding=API_NAME) + msg = msg or f"{self._msg} {self._msg_extra}".strip() + msg = msg.format(name=name, binding=binding, **msg_kwargs) + super().__init__(msg, name=name) + + +class QtModuleNotInOSError(QtModuleNotFoundError): + """Raised when a module is not supported on the current operating system.""" + + _msg = "{name} does not exist on this operating system." + + +class QtModuleNotInQtVersionError(QtModuleNotFoundError): + """Raised when a module is not implemented in the current Qt version.""" + + _msg = "{name} does not exist in {version}." + + def __init__(self, *, name, msg=None, **msg_kwargs): + global QT5, QT6 + version = "Qt5" if QT5 else "Qt6" + super().__init__(name=name, version=version) + + +class QtBindingMissingModuleError(QtModuleNotFoundError): + """Raised when a module is not supported by a given binding.""" + + _msg_extra = "It is not currently implemented in {binding}." + + +class QtBindingInNewerVersionError(QtModuleNotFoundError): + """Raised when a module is only supported in newer versions of a binding.""" + + _msg_extra = "It is only available in newer versions of {binding}." + + +class QtModuleNotInstalledError(QtModuleNotFoundError): + """Raise when a module is supported by the binding, but not installed.""" + + _msg_extra = "It must be installed separately" + + def __init__(self, *, missing_package=None, **superclass_kwargs): + self.missing_package = missing_package + if missing_package is not None: + self._msg_extra += " as {missing_package}." + super().__init__(missing_package=missing_package, **superclass_kwargs) + + +# Qt API environment variable name +QT_API = "QT_API" + +# Names of the expected PyQt5 api +PYQT5_API = ["pyqt5"] + +PYQT6_API = ["pyqt6"] + +# Names of the expected PySide2 api +PYSIDE2_API = ["pyside2"] + +# Names of the expected PySide6 api +PYSIDE6_API = ["pyside6"] + +# Minimum supported versions of Qt and the bindings +QT5_VERSION_MIN = PYQT5_VERSION_MIN = "5.9.0" +PYSIDE2_VERSION_MIN = "5.12.0" +QT6_VERSION_MIN = PYQT6_VERSION_MIN = PYSIDE6_VERSION_MIN = "6.2.0" + +QT_VERSION_MIN = QT5_VERSION_MIN +PYQT_VERSION_MIN = PYQT5_VERSION_MIN +PYSIDE_VERSION_MIN = PYSIDE2_VERSION_MIN + +# Detecting if a binding was specified by the user +binding_specified = QT_API in os.environ + +API_NAMES = { + "pyqt5": "PyQt5", + "pyside2": "PySide2", + "pyqt6": "PyQt6", + "pyside6": "PySide6", +} +API = os.environ.get(QT_API, "pyqt5").lower() +initial_api = API +if API not in API_NAMES: + raise PythonQtValueError( + f"Specified QT_API={QT_API.lower()!r} is not in valid options: " + f"{API_NAMES}", + ) is_old_pyqt = is_pyqt46 = False -PYQT5 = True -PYQT4 = PYSIDE = False +QT5 = PYQT5 = True +QT4 = QT6 = PYQT4 = PYQT6 = PYSIDE = PYSIDE2 = PYSIDE6 = False + +PYQT_VERSION = None +PYSIDE_VERSION = None +QT_VERSION = None -class PythonQtError(Exception): - """Error raise if no bindings could be selected""" - pass +def _parse_int(value): + """Convert a value into an integer""" + try: + return int(value) + except ValueError: + return 0 +def _parse_version(version): + """Parse a version into a comparable object""" + try: + from packaging.version import parse as _packaging_version_parse + except ImportError: + return _parse_version_internal(version) + else: + return _packaging_version_parse(version) + + +def _parse_version_internal(version): + """Parse a version string into a tuple of ints""" + return tuple(_parse_int(x) for x in version.split(".")) + + +# Unless `FORCE_QT_API` is set, use previously imported Qt Python bindings +if not os.environ.get("FORCE_QT_API"): + if "PyQt5" in sys.modules: + API = initial_api if initial_api in PYQT5_API else "pyqt5" + elif "PySide2" in sys.modules: + API = initial_api if initial_api in PYSIDE2_API else "pyside2" + elif "PyQt6" in sys.modules: + API = initial_api if initial_api in PYQT6_API else "pyqt6" + elif "PySide6" in sys.modules: + API = initial_api if initial_api in PYSIDE6_API else "pyside6" + if API in PYQT5_API: try: - from PyQt5.Qt import PYQT_VERSION_STR as PYQT_VERSION # analysis:ignore - from PyQt5.Qt import QT_VERSION_STR as QT_VERSION # analysis:ignore - PYSIDE_VERSION = None + from PyQt5.QtCore import ( + PYQT_VERSION_STR as PYQT_VERSION, + ) + from PyQt5.QtCore import ( + QT_VERSION_STR as QT_VERSION, + ) + + QT5 = PYQT5 = True + + if sys.platform == "darwin": + macos_version = _parse_version(platform.mac_ver()[0]) + qt_ver = _parse_version(QT_VERSION) + if macos_version < _parse_version( + "10.10", + ) and qt_ver >= _parse_version("5.9"): + raise PythonQtError( + "Qt 5.9 or higher only works in " + "macOS 10.10 or higher. Your " + "program will fail in this " + "system.", + ) + elif macos_version < _parse_version( + "10.11", + ) and qt_ver >= _parse_version("5.11"): + raise PythonQtError( + "Qt 5.11 or higher only works in " + "macOS 10.11 or higher. Your " + "program will fail in this " + "system.", + ) + + del macos_version + del qt_ver except ImportError: - API = os.environ['QT_API'] = 'pyqt' + API = "pyside2" + else: + os.environ[QT_API] = API -if API in PYQT4_API: +if API in PYSIDE2_API: try: - import sip - try: - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) - sip.setapi('QDate', 2) - sip.setapi('QDateTime', 2) - sip.setapi('QTextStream', 2) - sip.setapi('QTime', 2) - sip.setapi('QUrl', 2) - except AttributeError: - # PyQt < v4.6 - pass - from PyQt4.Qt import PYQT_VERSION_STR as PYQT_VERSION # analysis:ignore - from PyQt4.Qt import QT_VERSION_STR as QT_VERSION # analysis:ignore - PYSIDE_VERSION = None + from PySide2 import __version__ as PYSIDE_VERSION # analysis:ignore + from PySide2.QtCore import __version__ as QT_VERSION # analysis:ignore + PYQT5 = False - PYQT4 = True + QT5 = PYSIDE2 = True + + if sys.platform == "darwin": + macos_version = _parse_version(platform.mac_ver()[0]) + qt_ver = _parse_version(QT_VERSION) + if macos_version < _parse_version( + "10.11", + ) and qt_ver >= _parse_version("5.11"): + raise PythonQtError( + "Qt 5.11 or higher only works in " + "macOS 10.11 or higher. Your " + "program will fail in this " + "system.", + ) + + del macos_version + del qt_ver except ImportError: - API = os.environ['QT_API'] = 'pyside' + API = "pyqt6" else: - is_old_pyqt = PYQT_VERSION.startswith(('4.4', '4.5', '4.6', '4.7')) - is_pyqt46 = PYQT_VERSION.startswith('4.6') + os.environ[QT_API] = API -if API in PYSIDE_API: +if API in PYQT6_API: try: - from PySide import __version__ as PYSIDE_VERSION # analysis:ignore - from PySide.QtCore import __version__ as QT_VERSION # analysis:ignore - PYQT_VERSION = None - PYQT5 = False - PYSIDE = True + from PyQt6.QtCore import ( + PYQT_VERSION_STR as PYQT_VERSION, + ) + from PyQt6.QtCore import ( + QT_VERSION_STR as QT_VERSION, + ) + + QT5 = PYQT5 = False + QT6 = PYQT6 = True + + except ImportError: + API = "pyside6" + else: + os.environ[QT_API] = API + +if API in PYSIDE6_API: + try: + from PySide6 import __version__ as PYSIDE_VERSION # analysis:ignore + + if PYSIDE_VERSION == "6.8.0": + print( + "A known critical bug in PySide6 6.8.0 will cause your application to crash. " + "See https://github.com/spyder-ide/qtpy/issues/494", + ) + from PySide6.QtCore import __version__ as QT_VERSION # analysis:ignore + + QT5 = PYQT5 = False + QT6 = PYSIDE6 = True + except ImportError: - raise PythonQtError('No Qt bindings could be found') - -API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyqt4': 'PyQt4', - 'pyside': 'PySide'}[API] -if PYQT4: - import sip - try: - API_NAME += (" (API v{0})".format(sip.getapi('QString'))) - except AttributeError: - pass + raise QtBindingsNotFoundError from None + else: + os.environ[QT_API] = API + + +# If a correct API name is passed to QT_API and it could not be found, +# switches to another and informs through the warning +if initial_api != API and binding_specified: + warnings.warn( + f"Selected binding {initial_api!r} could not be found; " + f"falling back to {API!r}", + PythonQtWarning, + stacklevel=2, + ) + + +# Set display name of the Qt API +API_NAME = API_NAMES[API] + +with contextlib.suppress(ImportError, PythonQtError): + # QtDataVisualization backward compatibility (QtDataVisualization vs. QtDatavisualization) + # Only available for Qt5 bindings > 5.9 on Windows + from . import QtDataVisualization as QtDatavisualization # analysis:ignore + + +def _warn_old_minor_version(name, old_version, min_version): + """Warn if using a Qt or binding version no longer supported by QtPy.""" + warning_message = ( + f"{name} version {old_version} is not supported by QtPy. " + "To ensure your application works correctly with QtPy, " + f"please upgrade to {name} {min_version} or later." + ) + warnings.warn(warning_message, PythonQtWarning, stacklevel=2) + + +# Warn if using an End of Life or unsupported Qt API/binding minor version +if QT_VERSION: + if QT5 and (_parse_version(QT_VERSION) < _parse_version(QT5_VERSION_MIN)): + _warn_old_minor_version("Qt5", QT_VERSION, QT5_VERSION_MIN) + elif QT6 and ( + _parse_version(QT_VERSION) < _parse_version(QT6_VERSION_MIN) + ): + _warn_old_minor_version("Qt6", QT_VERSION, QT6_VERSION_MIN) + +if PYQT_VERSION: + if PYQT5 and ( + _parse_version(PYQT_VERSION) < _parse_version(PYQT5_VERSION_MIN) + ): + _warn_old_minor_version("PyQt5", PYQT_VERSION, PYQT5_VERSION_MIN) + elif PYQT6 and ( + _parse_version(PYQT_VERSION) < _parse_version(PYQT6_VERSION_MIN) + ): + _warn_old_minor_version("PyQt6", PYQT_VERSION, PYQT6_VERSION_MIN) +elif PYSIDE_VERSION: + if PYSIDE2 and ( + _parse_version(PYSIDE_VERSION) < _parse_version(PYSIDE2_VERSION_MIN) + ): + _warn_old_minor_version("PySide2", PYSIDE_VERSION, PYSIDE2_VERSION_MIN) + elif PYSIDE6 and ( + _parse_version(PYSIDE_VERSION) < _parse_version(PYSIDE6_VERSION_MIN) + ): + _warn_old_minor_version("PySide6", PYSIDE_VERSION, PYSIDE6_VERSION_MIN) diff --git a/qtpy/__main__.py b/qtpy/__main__.py new file mode 100644 index 00000000..a8f993c0 --- /dev/null +++ b/qtpy/__main__.py @@ -0,0 +1,18 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The QtPy Contributors +# +# Released under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Dev CLI entry point for QtPy, a compat layer for the Python Qt bindings.""" + +import qtpy.cli + + +def main(): + return qtpy.cli.main() + + +if __name__ == "__main__": + main() diff --git a/qtpy/_patch/qcombobox.py b/qtpy/_patch/qcombobox.py deleted file mode 100644 index ec7c01d1..00000000 --- a/qtpy/_patch/qcombobox.py +++ /dev/null @@ -1,101 +0,0 @@ -# The code below, as well as the associated test were adapted from -# qt-helpers, which was released under a 3-Clause BSD license: -# -# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the -# distribution. -# * Neither the name of the Glue project nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -def patch_qcombobox(QComboBox): - """ - In PySide, using Python objects as userData in QComboBox causes - Segmentation faults under certain conditions. Even in cases where it - doesn't, findData does not work correctly. Likewise, findData also does not - work correctly with Python objects when using PyQt4. On the other hand, - PyQt5 deals with this case correctly. We therefore patch QComboBox when - using PyQt4 and PySide to avoid issues. - """ - - from qtpy.QtGui import QIcon - from qtpy.QtCore import Qt, QObject - - class userDataWrapper(): - """ - This class is used to wrap any userData object. If we don't do this, - then certain types of objects can cause segmentation faults or issues - depending on whether/how __getitem__ is defined. - """ - def __init__(self, data): - self.data = data - - _addItem = QComboBox.addItem - - def addItem(self, *args, **kwargs): - if len(args) == 3 or (not isinstance(args[0], QIcon) - and len(args) == 2): - args, kwargs['userData'] = args[:-1], args[-1] - if 'userData' in kwargs: - kwargs['userData'] = userDataWrapper(kwargs['userData']) - _addItem(self, *args, **kwargs) - - _insertItem = QComboBox.insertItem - - def insertItem(self, *args, **kwargs): - if len(args) == 4 or (not isinstance(args[1], QIcon) - and len(args) == 3): - args, kwargs['userData'] = args[:-1], args[-1] - if 'userData' in kwargs: - kwargs['userData'] = userDataWrapper(kwargs['userData']) - _insertItem(self, *args, **kwargs) - - _setItemData = QComboBox.setItemData - - def setItemData(self, index, value, role=Qt.UserRole): - value = userDataWrapper(value) - _setItemData(self, index, value, role=role) - - _itemData = QComboBox.itemData - - def itemData(self, index, role=Qt.UserRole): - userData = _itemData(self, index, role=role) - if isinstance(userData, userDataWrapper): - userData = userData.data - return userData - - def findData(self, value): - for i in range(self.count()): - if self.itemData(i) == value: - return i - return -1 - - QComboBox.addItem = addItem - QComboBox.insertItem = insertItem - QComboBox.setItemData = setItemData - QComboBox.itemData = itemData - QComboBox.findData = findData \ No newline at end of file diff --git a/qtpy/_patch/qheaderview.py b/qtpy/_patch/qheaderview.py deleted file mode 100644 index 8bef8395..00000000 --- a/qtpy/_patch/qheaderview.py +++ /dev/null @@ -1,82 +0,0 @@ -def introduce_renamed_methods_qheaderview(QHeaderView): - - _isClickable = QHeaderView.isClickable - def sectionsClickable(self): - """ - QHeaderView.sectionsClickable() -> bool - """ - return _isClickable(self) - QHeaderView.sectionsClickable = sectionsClickable - def isClickable(self): - raise Exception('isClickable is only available in Qt4. Use ' - 'sectionsClickable instead.') - QHeaderView.isClickable = isClickable - - - _isMovable = QHeaderView.isMovable - def sectionsMovable(self): - """ - QHeaderView.sectionsMovable() -> bool - """ - return _isMovable(self) - QHeaderView.sectionsMovable = sectionsMovable - def isMovable(self): - raise Exception('isMovable is only available in Qt4. Use ' - 'sectionsMovable instead.') - QHeaderView.isMovable = isMovable - - - _resizeMode = QHeaderView.resizeMode - def sectionResizeMode(self, logicalIndex): - """ - QHeaderView.sectionResizeMode(int) -> QHeaderView.ResizeMode - """ - return _resizeMode(self, logicalIndex) - QHeaderView.sectionResizeMode = sectionResizeMode - def resizeMode(self, logicalIndex): - raise Exception('resizeMode is only available in Qt4. Use ' - 'sectionResizeMode instead.') - QHeaderView.resizeMode = resizeMode - - _setClickable = QHeaderView.setClickable - def setSectionsClickable(self, clickable): - """ - QHeaderView.setSectionsClickable(bool) - """ - return _setClickable(self, clickable) - QHeaderView.setSectionsClickable = setSectionsClickable - def setClickable(self, clickable): - raise Exception('setClickable is only available in Qt4. Use ' - 'setSectionsClickable instead.') - QHeaderView.setClickable = setClickable - - - _setMovable = QHeaderView.setMovable - def setSectionsMovable(self, movable): - """ - QHeaderView.setSectionsMovable(bool) - """ - return _setMovable(self, movable) - QHeaderView.setSectionsMovable = setSectionsMovable - def setMovable(self, movable): - raise Exception('setMovable is only available in Qt4. Use ' - 'setSectionsMovable instead.') - QHeaderView.setMovable = setMovable - - - _setResizeMode = QHeaderView.setResizeMode - def setSectionResizeMode(self, *args): - """ - QHeaderView.setSectionResizeMode(QHeaderView.ResizeMode) - QHeaderView.setSectionResizeMode(int, QHeaderView.ResizeMode) - """ - _setResizeMode(self, *args) - QHeaderView.setSectionResizeMode = setSectionResizeMode - def setResizeMode(self, *args): - raise Exception('setResizeMode is only available in Qt4. Use ' - 'setSectionResizeMode instead.') - QHeaderView.setResizeMode = setResizeMode - - - - diff --git a/qtpy/_utils.py b/qtpy/_utils.py new file mode 100644 index 00000000..4b6ee5df --- /dev/null +++ b/qtpy/_utils.py @@ -0,0 +1,195 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2023- The Spyder Development Team +# +# Released under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides utility functions for use by QtPy itself.""" +from functools import wraps +from typing import TYPE_CHECKING + +from . import QtModuleNotInstalledError + +if TYPE_CHECKING: + from .QtWidgets import QAction + + +def _wrap_missing_optional_dep_error( + attr_error, + *, + import_error, + wrapper=QtModuleNotInstalledError, + **wrapper_kwargs, +): + """Create a __cause__-chained wrapper error for a missing optional dep.""" + qtpy_error = wrapper(**wrapper_kwargs) + import_error.__cause__ = attr_error + qtpy_error.__cause__ = import_error + return qtpy_error + + +def getattr_missing_optional_dep(name, module_name, optional_names): + """Wrap AttributeError in a special error if it matches.""" + attr_error = AttributeError( + f"module {module_name!r} has no attribute {name!r}", + ) + if name in optional_names: + return _wrap_missing_optional_dep_error( + attr_error, + **optional_names[name], + ) + return attr_error + + +def possibly_static_exec(cls, *args, **kwargs): + """Call `self.exec` when `self` is given or a static method otherwise.""" + if not args and not kwargs: + # A special case (`cls.exec_()`) to avoid the function resolving error + return cls.exec() + if isinstance(args[0], cls): + if len(args) == 1 and not kwargs: + # A special case (`self.exec_()`) to avoid the function resolving error + return args[0].exec() + return args[0].exec(*args[1:], **kwargs) + + return cls.exec(*args, **kwargs) + + +def possibly_static_exec_(cls, *args, **kwargs): + """Call `self.exec` when `self` is given or a static method otherwise.""" + if not args and not kwargs: + # A special case (`cls.exec()`) to avoid the function resolving error + return cls.exec_() + if isinstance(args[0], cls): + if len(args) == 1 and not kwargs: + # A special case (`self.exec()`) to avoid the function resolving error + return args[0].exec_() + return args[0].exec_(*args[1:], **kwargs) + + return cls.exec_(*args, **kwargs) + + +def set_shortcut(self, shortcut, old_set_shortcut): + """Ensure that the type of `shortcut` is compatible to `QAction.setShortcut`.""" + from .QtCore import Qt + from .QtGui import QKeySequence + + if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int)): + shortcut = QKeySequence(shortcut) + old_set_shortcut(self, shortcut) + + +def set_shortcuts(self, shortcuts, old_set_shortcuts): + """Ensure that the type of `shortcuts` is compatible to `QAction.setShortcuts`.""" + from .QtCore import Qt + from .QtGui import QKeySequence + + if isinstance( + shortcuts, + (QKeySequence, QKeySequence.StandardKey, Qt.Key, int, str), + ): + shortcuts = (shortcuts,) + + shortcuts = tuple( + ( + QKeySequence(shortcut) + if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int)) + else shortcut + ) + for shortcut in shortcuts + ) + old_set_shortcuts(self, shortcuts) + + +def add_action(self, *args, old_add_action): + """Re-order arguments of `addAction` to backport compatibility with Qt>=6.3.""" + from .QtCore import QObject, Qt + from .QtGui import QIcon, QKeySequence + + action: QAction + icon: QIcon + text: str + shortcut: QKeySequence | QKeySequence.StandardKey | Qt.Key | str | int + receiver: QObject + member: bytes + + if all( + isinstance(arg, t) + for arg, t in zip( + args, + [ + str, + (QKeySequence, QKeySequence.StandardKey, Qt.Key, str, int), + QObject, + bytes, + ], + ) + ): + if len(args) == 2: + text, shortcut = args + action = old_add_action(self, text) + action.setShortcut(shortcut) + elif len(args) == 3: + text, shortcut, receiver = args + action = old_add_action(self, text, receiver) + action.setShortcut(shortcut) + elif len(args) == 4: + text, shortcut, receiver, member = args + action = old_add_action(self, text, receiver, member, shortcut) + else: + action = old_add_action(self, *args) + elif all( + isinstance(arg, t) + for arg, t in zip( + args, + [ + QIcon, + str, + (QKeySequence, QKeySequence.StandardKey, Qt.Key, str, int), + QObject, + bytes, + ], + ) + ): + if len(args) == 3: + icon, text, shortcut = args + action = old_add_action(self, icon, text) + action.setShortcut(shortcut) + elif len(args) == 4: + icon, text, shortcut, receiver = args + action = old_add_action(self, icon, text, receiver) + action.setShortcut(shortcut) + elif len(args) == 5: + icon, text, shortcut, receiver, member = args + action = old_add_action( + self, + icon, + text, + receiver, + member, + shortcut, + ) + else: + action = old_add_action(self, *args) + else: + action = old_add_action(self, *args) + + return action + + +def static_method_kwargs_wrapper(func, from_kwarg_name, to_kwarg_name): + """ + Helper function to manage `from_kwarg_name` to `to_kwarg_name` kwargs name changes in static methods. + + Makes static methods accept the `from_kwarg_name` kwarg as `to_kwarg_name`. + """ + + @staticmethod + @wraps(func) + def _from_kwarg_name_to_kwarg_name_(*args, **kwargs): + if from_kwarg_name in kwargs: + kwargs[to_kwarg_name] = kwargs.pop(from_kwarg_name) + return func(*args, **kwargs) + + return _from_kwarg_name_to_kwarg_name_ diff --git a/qtpy/_version.py b/qtpy/_version.py deleted file mode 100644 index a89112b0..00000000 --- a/qtpy/_version.py +++ /dev/null @@ -1,2 +0,0 @@ -version_info = (1, 2, 0, 'dev0') -__version__ = '.'.join(map(str, version_info)) diff --git a/qtpy/cli.py b/qtpy/cli.py new file mode 100644 index 00000000..95da0252 --- /dev/null +++ b/qtpy/cli.py @@ -0,0 +1,166 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The QtPy Contributors +# +# Released under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provide a CLI to allow configuring developer settings, including mypy.""" + +# Standard library imports +import argparse +import json +import textwrap + + +def print_version(): + """Print the current version of the package.""" + import qtpy + + print("QtPy version", qtpy.__version__) + + +def get_api_status(): + """Get the status of each Qt API usage.""" + import qtpy + + return {name: name == qtpy.API for name in qtpy.API_NAMES} + + +def generate_mypy_args(): + """Generate a string with always-true/false args to pass to mypy.""" + options = {False: "--always-false", True: "--always-true"} + + apis_active = get_api_status() + return " ".join( + f"{options[is_active]}={name.upper()}" + for name, is_active in apis_active.items() + ) + + +def generate_pyright_config_json(): + """Generate Pyright config to be used in `pyrightconfig.json`.""" + apis_active = get_api_status() + + return json.dumps( + { + "defineConstant": { + name.upper(): is_active + for name, is_active in apis_active.items() + }, + }, + ) + + +def generate_pyright_config_toml(): + """Generate a Pyright config to be used in `pyproject.toml`.""" + apis_active = get_api_status() + + return "[tool.pyright.defineConstant]\n" + "\n".join( + f"{name.upper()} = {str(is_active).lower()}" + for name, is_active in apis_active.items() + ) + + +def print_mypy_args(): + """Print the generated mypy args to stdout.""" + print(generate_mypy_args()) + + +def print_pyright_config_json(): + """Print the generated Pyright JSON config to stdout.""" + print(generate_pyright_config_json()) + + +def print_pyright_config_toml(): + """Print the generated Pyright TOML config to stdout.""" + print(generate_pyright_config_toml()) + + +def print_pyright_configs(): + """Print the generated Pyright configs to stdout.""" + print("pyrightconfig.json:") + print_pyright_config_json() + print() + print("pyproject.toml:") + print_pyright_config_toml() + + +def generate_arg_parser(): + """Generate the argument parser for the dev CLI for QtPy.""" + parser = argparse.ArgumentParser( + description="Features to support development with QtPy.", + ) + parser.set_defaults(func=parser.print_help) + + parser.add_argument( + "--version", + action="store_const", + dest="func", + const=print_version, + help="If passed, will print the version and exit", + ) + + cli_subparsers = parser.add_subparsers( + title="Subcommands", + help="Subcommand to run", + metavar="Subcommand", + ) + + # Parser for the MyPy args subcommand + mypy_args_parser = cli_subparsers.add_parser( + name="mypy-args", + help="Generate command line arguments for using mypy with QtPy.", + formatter_class=argparse.RawTextHelpFormatter, + description=textwrap.dedent( + """ + Generate command line arguments for using mypy with QtPy. + + This will generate strings similar to the following + which help guide mypy through which library QtPy would have used + so that mypy can get the proper underlying type hints. + + --always-false=PYQT5 --always-false=PYQT6 --always-true=PYSIDE2 --always-false=PYSIDE6 + + It can be used as follows on Bash or a similar shell: + + mypy --package mypackage $(qtpy mypy-args) + """, + ), + ) + mypy_args_parser.set_defaults(func=print_mypy_args) + + # Parser for the Pyright config subcommand + pyright_config_parser = cli_subparsers.add_parser( + name="pyright-config", + help="Generate Pyright config for using Pyright with QtPy.", + formatter_class=argparse.RawTextHelpFormatter, + description=textwrap.dedent( + """ + Generate Pyright config for using Pyright with QtPy. + + This will generate config sections to be included in a Pyright + config file (either `pyrightconfig.json` or `pyproject.toml`) + which help guide Pyright through which library QtPy would have used + so that Pyright can get the proper underlying type hints. + + """, + ), + ) + pyright_config_parser.set_defaults(func=print_pyright_configs) + + return parser + + +def main(args=None): + """Run the development CLI for QtPy.""" + parser = generate_arg_parser() + parsed_args = parser.parse_args(args=args) + + reserved_params = {"func"} + cleaned_args = { + key: value + for key, value in vars(parsed_args).items() + if key not in reserved_params + } + parsed_args.func(**cleaned_args) diff --git a/qtpy/compat.py b/qtpy/compat.py index ae38090e..1d66884c 100644 --- a/qtpy/compat.py +++ b/qtpy/compat.py @@ -1,196 +1,239 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2009- The Spyder Development Team -# Licensed under the terms of the MIT License - -""" -Compatibility functions -""" - -from __future__ import print_function -import sys -import collections - -from qtpy import PYQT4 -from qtpy.QtWidgets import QFileDialog -from qtpy.py3compat import is_text_string, to_text_string, TEXT_TYPES - - -# ============================================================================= -# QVariant conversion utilities -# ============================================================================= -PYQT_API_1 = False -if PYQT4: - import sip - try: - PYQT_API_1 = sip.getapi('QVariant') == 1 # PyQt API #1 - except AttributeError: - # PyQt =v4.4 (API #1 and #2) and PySide >=v1.0""" - # Calling QFileDialog static method - if sys.platform == "win32": - # On Windows platforms: redirect standard outputs - _temp1, _temp2 = sys.stdout, sys.stderr - sys.stdout, sys.stderr = None, None - try: - result = QFileDialog.getExistingDirectory(parent, caption, basedir, - options) - finally: - if sys.platform == "win32": - # On Windows platforms: restore standard outputs - sys.stdout, sys.stderr = _temp1, _temp2 - if not is_text_string(result): - # PyQt API #1 - result = to_text_string(result) - return result - - -def _qfiledialog_wrapper(attr, parent=None, caption='', basedir='', - filters='', selectedfilter='', options=None): - if options is None: - options = QFileDialog.Options(0) - try: - # PyQt =v4.6 - QString = None # analysis:ignore - tuple_returned = True - try: - # PyQt >=v4.6 - func = getattr(QFileDialog, attr+'AndFilter') - except AttributeError: - # PySide or PyQt =v4.6 - output, selectedfilter = result - else: - # PyQt =v4.4 (API #1 and #2) and PySide >=v1.0""" - return _qfiledialog_wrapper('getOpenFileName', parent=parent, - caption=caption, basedir=basedir, - filters=filters, selectedfilter=selectedfilter, - options=options) - - -def getopenfilenames(parent=None, caption='', basedir='', filters='', - selectedfilter='', options=None): - """Wrapper around QtGui.QFileDialog.getOpenFileNames static method - Returns a tuple (filenames, selectedfilter) -- when dialog box is canceled, - returns a tuple (empty list, empty string) - Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" - return _qfiledialog_wrapper('getOpenFileNames', parent=parent, - caption=caption, basedir=basedir, - filters=filters, selectedfilter=selectedfilter, - options=options) - - -def getsavefilename(parent=None, caption='', basedir='', filters='', - selectedfilter='', options=None): - """Wrapper around QtGui.QFileDialog.getSaveFileName static method - Returns a tuple (filename, selectedfilter) -- when dialog box is canceled, - returns a tuple of empty strings - Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" - return _qfiledialog_wrapper('getSaveFileName', parent=parent, - caption=caption, basedir=basedir, - filters=filters, selectedfilter=selectedfilter, - options=options) +# +# Copyright © 2009- The Spyder Development Team +# Licensed under the terms of the MIT License + +""" +Compatibility functions +""" + +import enum +import sys + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingsNotFoundError, +) +from .QtWidgets import QFileDialog + +TEXT_TYPES = (str,) + + +def is_text_string(obj): + """Return True if `obj` is a text string, False if it is anything else, + like binary data.""" + return isinstance(obj, str) + + +def to_text_string(obj, encoding=None): + """Convert `obj` to (unicode) text string""" + if encoding is None: + return str(obj) + if isinstance(obj, str): + # In case this function is not used properly, this could happen + return obj + + return str(obj, encoding) + + +# ============================================================================= +# QVariant conversion utilities +# ============================================================================= +PYQT_API_1 = False + + +def to_qvariant(obj=None): # analysis:ignore + """Convert Python object to QVariant + This is a transitional function from PyQt API#1 (QVariant exist) + to PyQt API#2 and Pyside (QVariant does not exist)""" + return obj + + +def from_qvariant(qobj=None, pytype=None): # analysis:ignore + """Convert QVariant object to Python object + This is a transitional function from PyQt API #1 (QVariant exist) + to PyQt API #2 and Pyside (QVariant does not exist)""" + return qobj + + +# ============================================================================= +# Wrappers around QFileDialog static methods +# ============================================================================= +def getexistingdirectory( + parent=None, + caption="", + basedir="", + options=QFileDialog.ShowDirsOnly, +): + """Wrapper around QtGui.QFileDialog.getExistingDirectory static method + Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" + # Calling QFileDialog static method + if sys.platform == "win32": + # On Windows platforms: redirect standard outputs + _temp1, _temp2 = sys.stdout, sys.stderr + sys.stdout, sys.stderr = None, None + try: + result = QFileDialog.getExistingDirectory( + parent, + caption, + basedir, + options, + ) + finally: + if sys.platform == "win32": + # On Windows platforms: restore standard outputs + sys.stdout, sys.stderr = _temp1, _temp2 + if not is_text_string(result): + # PyQt API #1 + result = to_text_string(result) + return result + + +def _qfiledialog_wrapper( + attr, + parent=None, + caption="", + basedir="", + filters="", + selectedfilter="", + options=None, +): + if options is None: + options = QFileDialog.Option(0) + + func = getattr(QFileDialog, attr) + + # Calling QFileDialog static method + if sys.platform == "win32": + # On Windows platforms: redirect standard outputs + _temp1, _temp2 = sys.stdout, sys.stderr + sys.stdout, sys.stderr = None, None + result = func(parent, caption, basedir, filters, selectedfilter, options) + if sys.platform == "win32": + # On Windows platforms: restore standard outputs + sys.stdout, sys.stderr = _temp1, _temp2 + + output, selectedfilter = result + + # Always returns the tuple (output, selectedfilter) + return output, selectedfilter + + +def getopenfilename( + parent=None, + caption="", + basedir="", + filters="", + selectedfilter="", + options=None, +): + """Wrapper around QtGui.QFileDialog.getOpenFileName static method + Returns a tuple (filename, selectedfilter) -- when dialog box is canceled, + returns a tuple of empty strings + Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" + return _qfiledialog_wrapper( + "getOpenFileName", + parent=parent, + caption=caption, + basedir=basedir, + filters=filters, + selectedfilter=selectedfilter, + options=options, + ) + + +def getopenfilenames( + parent=None, + caption="", + basedir="", + filters="", + selectedfilter="", + options=None, +): + """Wrapper around QtGui.QFileDialog.getOpenFileNames static method + Returns a tuple (filenames, selectedfilter) -- when dialog box is canceled, + returns a tuple (empty list, empty string) + Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" + return _qfiledialog_wrapper( + "getOpenFileNames", + parent=parent, + caption=caption, + basedir=basedir, + filters=filters, + selectedfilter=selectedfilter, + options=options, + ) + + +def getsavefilename( + parent=None, + caption="", + basedir="", + filters="", + selectedfilter="", + options=None, +): + """Wrapper around QtGui.QFileDialog.getSaveFileName static method + Returns a tuple (filename, selectedfilter) -- when dialog box is canceled, + returns a tuple of empty strings + Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" + return _qfiledialog_wrapper( + "getSaveFileName", + parent=parent, + caption=caption, + basedir=basedir, + filters=filters, + selectedfilter=selectedfilter, + options=options, + ) + + +# ============================================================================= +def isalive(obj): + """Wrapper around sip.isdeleted and shiboken.isValid which tests whether + an object is currently alive.""" + if PYQT5 or PYQT6: + from . import sip + + return not sip.isdeleted(obj) + if PYSIDE2 or PYSIDE6: + from . import shiboken + + return shiboken.isValid(obj) + return None + + +# ============================================================================= +def getenumasint(enum_value): + """Get the integer value of a Qt enum + For example: + Qt.AlignmentFlag.AlignBaseline -> 256 + Qt.WidgetAttribute.WA_AcceptDrops -> 78 + If an integer is passed in, simply return it. + PySide2's enums are themselves classes, not enum values per se, so if + we get an integer or a class, return the class. + """ + if isinstance(enum_value, enum.Enum): + if PYSIDE2 or PYQT5: + return int(enum_value) + return enum_value.value + return enum_value + + +# ============================================================================= +def getenumfromint(enum_class, i): + """Get the Qt enum value from an integer""" + return enum_class(i) + + +# ============================================================================= +def getimagebytes(qimage): + if PYQT5: + return qimage.bits().asstring(qimage.byteCount()) + if PYQT6: + return qimage.bits().asstring(qimage.sizeInBytes()) + if PYSIDE2 or PYSIDE6: + return qimage.bits().tobytes() + raise QtBindingsNotFoundError diff --git a/qtpy/enums_compat.py b/qtpy/enums_compat.py new file mode 100644 index 00000000..89a7d114 --- /dev/null +++ b/qtpy/enums_compat.py @@ -0,0 +1,39 @@ +# Copyright © 2009- The Spyder Development Team +# Copyright © 2012- University of North Carolina at Chapel Hill +# Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') +# Ogi Moore ('ognyan.moore@%s.com' % 'gmail') +# KIU Shueng Chuan ('nixchuan@%s.com' % 'gmail') +# Licensed under the terms of the MIT License + +""" +Compatibility functions for scoped and unscoped enum access. +""" + +from . import PYQT6 + +if PYQT6: + import enum + + from . import sip + + def promote_enums(module): + """ + Search enums in the given module and allow unscoped access. + + Taken from: + https://github.com/pyqtgraph/pyqtgraph/blob/pyqtgraph-0.12.1/pyqtgraph/Qt.py#L331-L377 + and adapted to also copy enum values aliased under different names. + + """ + class_names = [name for name in dir(module) if name.startswith("Q")] + for class_name in class_names: + klass = getattr(module, class_name) + if not isinstance(klass, sip.wrappertype): + continue + attrib_names = [name for name in dir(klass) if name[0].isupper()] + for attrib_name in attrib_names: + attrib = getattr(klass, attrib_name) + if not isinstance(attrib, enum.EnumMeta): + continue + for name, value in attrib.__members__.items(): + setattr(klass, name, value) diff --git a/qtpy/_patch/__init__.py b/qtpy/py.typed similarity index 100% rename from qtpy/_patch/__init__.py rename to qtpy/py.typed diff --git a/qtpy/py3compat.py b/qtpy/py3compat.py deleted file mode 100755 index 4d393bd3..00000000 --- a/qtpy/py3compat.py +++ /dev/null @@ -1,261 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © 2012-2013 Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyderlib/__init__.py for details) - -""" -spyderlib.py3compat -------------------- - -Transitional module providing compatibility functions intended to help -migrating from Python 2 to Python 3. - -This module should be fully compatible with: - * Python >=v2.6 - * Python 3 -""" - -from __future__ import print_function - -import sys -import os - -PY2 = sys.version[0] == '2' -PY3 = sys.version[0] == '3' - - -# ============================================================================= -# Data types -# ============================================================================= -if PY2: - # Python 2 - TEXT_TYPES = (str, unicode) - INT_TYPES = (int, long) -else: - # Python 3 - TEXT_TYPES = (str,) - INT_TYPES = (int,) -NUMERIC_TYPES = tuple(list(INT_TYPES) + [float, complex]) - - -# ============================================================================= -# Renamed/Reorganized modules -# ============================================================================= -if PY2: - # Python 2 - import __builtin__ as builtins - import ConfigParser as configparser - try: - import _winreg as winreg - except ImportError: - pass - from sys import maxint as maxsize - try: - import CStringIO as io - except ImportError: - import StringIO as io - try: - import cPickle as pickle - except ImportError: - import pickle - from UserDict import DictMixin as MutableMapping - import thread as _thread - import repr as reprlib -else: - # Python 3 - import builtins - import configparser - try: - import winreg - except ImportError: - pass - from sys import maxsize - import io - import pickle - from collections import MutableMapping - import _thread - import reprlib - - -# ============================================================================= -# Strings -# ============================================================================= -if PY2: - # Python 2 - import codecs - - def u(obj): - """Make unicode object""" - return codecs.unicode_escape_decode(obj)[0] -else: - # Python 3 - def u(obj): - """Return string as it is""" - return obj - - -def is_text_string(obj): - """Return True if `obj` is a text string, False if it is anything else, - like binary data (Python 3) or QString (Python 2, PyQt API #1)""" - if PY2: - # Python 2 - return isinstance(obj, basestring) - else: - # Python 3 - return isinstance(obj, str) - - -def is_binary_string(obj): - """Return True if `obj` is a binary string, False if it is anything else""" - if PY2: - # Python 2 - return isinstance(obj, str) - else: - # Python 3 - return isinstance(obj, bytes) - - -def is_string(obj): - """Return True if `obj` is a text or binary Python string object, - False if it is anything else, like a QString (Python 2, PyQt API #1)""" - return is_text_string(obj) or is_binary_string(obj) - - -def is_unicode(obj): - """Return True if `obj` is unicode""" - if PY2: - # Python 2 - return isinstance(obj, unicode) - else: - # Python 3 - return isinstance(obj, str) - - -def to_text_string(obj, encoding=None): - """Convert `obj` to (unicode) text string""" - if PY2: - # Python 2 - if encoding is None: - return unicode(obj) - else: - return unicode(obj, encoding) - else: - # Python 3 - if encoding is None: - return str(obj) - elif isinstance(obj, str): - # In case this function is not used properly, this could happen - return obj - else: - return str(obj, encoding) - - -def to_binary_string(obj, encoding=None): - """Convert `obj` to binary string (bytes in Python 3, str in Python 2)""" - if PY2: - # Python 2 - if encoding is None: - return str(obj) - else: - return obj.encode(encoding) - else: - # Python 3 - return bytes(obj, 'utf-8' if encoding is None else encoding) - - -# ============================================================================= -# Function attributes -# ============================================================================= -def get_func_code(func): - """Return function code object""" - if PY2: - # Python 2 - return func.func_code - else: - # Python 3 - return func.__code__ - - -def get_func_name(func): - """Return function name""" - if PY2: - # Python 2 - return func.func_name - else: - # Python 3 - return func.__name__ - - -def get_func_defaults(func): - """Return function default argument values""" - if PY2: - # Python 2 - return func.func_defaults - else: - # Python 3 - return func.__defaults__ - - -# ============================================================================= -# Special method attributes -# ============================================================================= -def get_meth_func(obj): - """Return method function object""" - if PY2: - # Python 2 - return obj.im_func - else: - # Python 3 - return obj.__func__ - - -def get_meth_class_inst(obj): - """Return method class instance""" - if PY2: - # Python 2 - return obj.im_self - else: - # Python 3 - return obj.__self__ - - -def get_meth_class(obj): - """Return method class""" - if PY2: - # Python 2 - return obj.im_class - else: - # Python 3 - return obj.__self__.__class__ - - -# ============================================================================= -# Misc. -# ============================================================================= -if PY2: - # Python 2 - input = raw_input - getcwd = os.getcwdu - cmp = cmp - import string - str_lower = string.lower - from itertools import izip_longest as zip_longest -else: - # Python 3 - input = input - getcwd = os.getcwd - - def cmp(a, b): - return (a > b) - (a < b) - str_lower = str.lower - from itertools import zip_longest - - -def qbytearray_to_str(qba): - """Convert QByteArray object to str in a way compatible with Python 2/3""" - return str(bytes(qba.toHex().data()).decode()) - - -if __name__ == '__main__': - pass diff --git a/qtpy/shiboken.py b/qtpy/shiboken.py new file mode 100644 index 00000000..3e20a0c8 --- /dev/null +++ b/qtpy/shiboken.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides access to shiboken.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5 or PYQT6: + raise QtBindingMissingModuleError(name="shiboken") +elif PYSIDE2: + from shiboken2 import * +elif PYSIDE6: + from shiboken6 import * diff --git a/qtpy/sip.py b/qtpy/sip.py new file mode 100644 index 00000000..205538c7 --- /dev/null +++ b/qtpy/sip.py @@ -0,0 +1,23 @@ +# ----------------------------------------------------------------------------- +# Copyright © 2009- The Spyder Development Team +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Provides access to sip.""" + +from . import ( + PYQT5, + PYQT6, + PYSIDE2, + PYSIDE6, + QtBindingMissingModuleError, +) + +if PYQT5: + from PyQt5.sip import * +elif PYQT6: + from PyQt6.sip import * +elif PYSIDE2 or PYSIDE6: + raise QtBindingMissingModuleError(name="sip") diff --git a/qtpy/tests/conftest.py b/qtpy/tests/conftest.py deleted file mode 100644 index c31a78aa..00000000 --- a/qtpy/tests/conftest.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - - -def pytest_configure(config): - """ - This function gets run by py.test at the very start - """ - - if 'USE_QT_API' in os.environ: - os.environ['QT_API'] = os.environ['USE_QT_API'].lower() - - # We need to import qtpy here to make sure that the API versions get set - # straight away. - import qtpy - - -def pytest_report_header(config): - """ - This function is used by py.test to insert a customized header into the - test report. - """ - - versions = os.linesep - versions += 'PyQt4: ' - - try: - from PyQt4 import Qt - versions += "PyQt: {0} - Qt: {1}".format(Qt.PYQT_VERSION_STR, Qt.QT_VERSION_STR) - except ImportError: - versions += 'not installed' - except AttributeError: - versions += 'unknown version' - - versions += os.linesep - versions += 'PyQt5: ' - - try: - from PyQt5 import Qt - versions += "PyQt: {0} - Qt: {1}".format(Qt.PYQT_VERSION_STR, Qt.QT_VERSION_STR) - except ImportError: - versions += 'not installed' - except AttributeError: - versions += 'unknown version' - - versions += os.linesep - versions += 'PySide: ' - - try: - import PySide - from PySide import QtCore - versions += "PySide: {0} - Qt: {1}".format(PySide.__version__, QtCore.__version__) - except ImportError: - versions += 'not installed' - except AttributeError: - versions += 'unknown version' - - versions += os.linesep - - return versions diff --git a/qtpy/tests/runtests.py b/qtpy/tests/runtests.py deleted file mode 100755 index b54fbb45..00000000 --- a/qtpy/tests/runtests.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright © 2015- The Spyder Development Team -# -# Licensed under the terms of the MIT License -# ---------------------------------------------------------------------------- - -"""File for running tests programmatically.""" - -# Standard library imports -import sys - -# Third party imports -import qtpy # to ensure that Qt4 uses API v2 -import pytest - - -def main(): - """Run pytest tests.""" - errno = pytest.main(['-x', 'qtpy', '-v', '-rw', '--durations=10', - '--cov=qtpy', '--cov-report=term-missing']) - sys.exit(errno) - -if __name__ == '__main__': - main() diff --git a/qtpy/tests/test_main.py b/qtpy/tests/test_main.py deleted file mode 100644 index 63d1439e..00000000 --- a/qtpy/tests/test_main.py +++ /dev/null @@ -1,71 +0,0 @@ -import os - -from qtpy import QtCore, QtGui, QtWidgets, QtWebEngineWidgets - - -def assert_pyside(): - """ - Make sure that we are using PySide - """ - import PySide - assert QtCore.QEvent is PySide.QtCore.QEvent - assert QtGui.QPainter is PySide.QtGui.QPainter - assert QtWidgets.QWidget is PySide.QtGui.QWidget - assert QtWebEngineWidgets.QWebEnginePage is PySide.QtWebKit.QWebPage - - -def assert_pyqt4(): - """ - Make sure that we are using PyQt4 - """ - import PyQt4 - assert QtCore.QEvent is PyQt4.QtCore.QEvent - assert QtGui.QPainter is PyQt4.QtGui.QPainter - assert QtWidgets.QWidget is PyQt4.QtGui.QWidget - assert QtWebEngineWidgets.QWebEnginePage is PyQt4.QtWebKit.QWebPage - - -def assert_pyqt5(): - """ - Make sure that we are using PyQt5 - """ - import PyQt5 - assert QtCore.QEvent is PyQt5.QtCore.QEvent - assert QtGui.QPainter is PyQt5.QtGui.QPainter - assert QtWidgets.QWidget is PyQt5.QtWidgets.QWidget - if QtWebEngineWidgets.WEBENGINE: - assert QtWebEngineWidgets.QWebEnginePage is PyQt5.QtWebEngineWidgets.QWebEnginePage - else: - assert QtWebEngineWidgets.QWebEnginePage is PyQt5.QtWebKitWidgets.QWebPage - - -def test_qt_api(): - """ - If QT_API is specified, we check that the correct Qt wrapper was used - """ - - QT_API = os.environ.get('QT_API', '').lower() - - if QT_API == 'pyside': - assert_pyside() - elif QT_API in ('pyqt', 'pyqt4'): - assert_pyqt4() - elif QT_API == 'pyqt5': - assert_pyqt5() - else: - # If the tests are run locally, USE_QT_API and QT_API may not be - # defined, but we still want to make sure qtpy is behaving sensibly. - # We should then be loading, in order of decreasing preference, PyQt5, - # PyQt4, and PySide. - try: - import PyQt5 - except ImportError: - try: - import PyQt4 - except ImportError: - import PySide - assert_pyside() - else: - assert_pyqt4() - else: - assert_pyqt5() diff --git a/qtpy/tests/test_patch_qcombobox.py b/qtpy/tests/test_patch_qcombobox.py deleted file mode 100644 index 04a83370..00000000 --- a/qtpy/tests/test_patch_qcombobox.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import absolute_import - -from qtpy import QtGui, QtWidgets - - -def get_qapp(icon_path=None): - qapp = QtWidgets.QApplication.instance() - if qapp is None: - qapp = QtWidgets.QApplication(['']) - return qapp - - -class Data(object): - """ - Test class to store in userData. The __getitem__ is needed in order to - reproduce the segmentation fault. - """ - def __getitem__(self, item): - raise ValueError("Failing") - - -def test_patched_qcombobox(): - """ - In PySide, using Python objects as userData in QComboBox causes - Segmentation faults under certain conditions. Even in cases where it - doesn't, findData does not work correctly. Likewise, findData also - does not work correctly with Python objects when using PyQt4. On the - other hand, PyQt5 deals with this case correctly. We therefore patch - QComboBox when using PyQt4 and PySide to avoid issues. - """ - - app = get_qapp() - - data1 = Data() - data2 = Data() - data3 = Data() - data4 = Data() - data5 = Data() - data6 = Data() - - icon1 = QtGui.QIcon() - icon2 = QtGui.QIcon() - - widget = QtWidgets.QComboBox() - widget.addItem('a', data1) - widget.insertItem(0, 'b', data2) - widget.addItem('c', data1) - widget.setItemData(2, data3) - widget.addItem(icon1, 'd', data4) - widget.insertItem(3, icon2, 'e', data5) - widget.addItem(icon1, 'f') - widget.insertItem(5, icon2, 'g') - - widget.show() - - assert widget.findData(data1) == 1 - assert widget.findData(data2) == 0 - assert widget.findData(data3) == 2 - assert widget.findData(data4) == 4 - assert widget.findData(data5) == 3 - assert widget.findData(data6) == -1 - - assert widget.itemData(0) == data2 - assert widget.itemData(1) == data1 - assert widget.itemData(2) == data3 - assert widget.itemData(3) == data5 - assert widget.itemData(4) == data4 - assert widget.itemData(5) is None - assert widget.itemData(6) is None - - assert widget.itemText(0) == 'b' - assert widget.itemText(1) == 'a' - assert widget.itemText(2) == 'c' - assert widget.itemText(3) == 'e' - assert widget.itemText(4) == 'd' - assert widget.itemText(5) == 'g' - assert widget.itemText(6) == 'f' - - -def test_model_item(): - """ - This is a regression test for an issue that caused the call to item(0) - below to trigger segmentation faults in PySide. The issue is - non-deterministic when running the call once, so we include a loop to make - sure that we trigger the fault. - """ - app = get_qapp() - combo = QtWidgets.QComboBox() - label_data = [('a', None)] - for iter in range(10000): - combo.clear() - for i, (label, data) in enumerate(label_data): - combo.addItem(label, userData=data) - model = combo.model() - model.item(0) diff --git a/qtpy/tests/test_patch_qheaderview.py b/qtpy/tests/test_patch_qheaderview.py deleted file mode 100644 index 6f30c337..00000000 --- a/qtpy/tests/test_patch_qheaderview.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import absolute_import - -from qtpy import PYSIDE, PYQT4 -from qtpy.QtWidgets import QApplication -from qtpy.QtWidgets import QHeaderView -from qtpy.QtCore import Qt -from qtpy.QtCore import QAbstractListModel - -import pytest - -def get_qapp(icon_path=None): - qapp = QApplication.instance() - if qapp is None: - qapp = QApplication(['']) - return qapp - -def test_patched_qheaderview(): - """ - This will test whether QHeaderView has the new methods introduced in Qt5. - It will then create an instance of QHeaderView and test that no exceptions - are raised and that some basic behaviour works. - """ - assert QHeaderView.sectionsClickable is not None - assert QHeaderView.sectionsMovable is not None - assert QHeaderView.sectionResizeMode is not None - assert QHeaderView.setSectionsClickable is not None - assert QHeaderView.setSectionsMovable is not None - assert QHeaderView.setSectionResizeMode is not None - - # setup a model and add it to a headerview - qapp = get_qapp() - headerview = QHeaderView(Qt.Horizontal) - class Model(QAbstractListModel): - pass - model = Model() - headerview.setModel(model) - assert headerview.count() == 1 - - # test it - assert isinstance(headerview.sectionsClickable(), bool) - assert isinstance(headerview.sectionsMovable(), bool) - if PYSIDE: - assert isinstance(headerview.sectionResizeMode(0), - QHeaderView.ResizeMode) - else: - assert isinstance(headerview.sectionResizeMode(0), int) - - headerview.setSectionsClickable(True) - assert headerview.sectionsClickable() == True - headerview.setSectionsClickable(False) - assert headerview.sectionsClickable() == False - - headerview.setSectionsMovable(True) - assert headerview.sectionsMovable() == True - headerview.setSectionsMovable(False) - assert headerview.sectionsMovable() == False - - headerview.setSectionResizeMode(QHeaderView.Interactive) - assert headerview.sectionResizeMode(0) == QHeaderView.Interactive - headerview.setSectionResizeMode(QHeaderView.Fixed) - assert headerview.sectionResizeMode(0) == QHeaderView.Fixed - headerview.setSectionResizeMode(QHeaderView.Stretch) - assert headerview.sectionResizeMode(0) == QHeaderView.Stretch - headerview.setSectionResizeMode(QHeaderView.ResizeToContents) - assert headerview.sectionResizeMode(0) == QHeaderView.ResizeToContents - - headerview.setSectionResizeMode(0, QHeaderView.Interactive) - assert headerview.sectionResizeMode(0) == QHeaderView.Interactive - headerview.setSectionResizeMode(0, QHeaderView.Fixed) - assert headerview.sectionResizeMode(0) == QHeaderView.Fixed - headerview.setSectionResizeMode(0, QHeaderView.Stretch) - assert headerview.sectionResizeMode(0) == QHeaderView.Stretch - headerview.setSectionResizeMode(0, QHeaderView.ResizeToContents) - assert headerview.sectionResizeMode(0) == QHeaderView.ResizeToContents - - # test that the old methods in Qt4 raise exceptions - if PYQT4 or PYSIDE: - with pytest.raises(Exception): - headerview.isClickable() - with pytest.raises(Exception): - headerview.isMovable() - with pytest.raises(Exception): - headerview.resizeMode(0) - with pytest.raises(Exception): - headerview.setClickable(True) - with pytest.raises(Exception): - headerview.setMovableClickable(True) - with pytest.raises(Exception): - headerview.setResizeMode(0, QHeaderView.Interactive) - - diff --git a/qtpy/tests/test_qtmultimedia.py b/qtpy/tests/test_qtmultimedia.py deleted file mode 100644 index a718d188..00000000 --- a/qtpy/tests/test_qtmultimedia.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import absolute_import - -from qtpy import QtMultimedia - - -def test_qtmultimedia(): - """Test the qtpy.QtMultimedia namespace""" - assert QtMultimedia.QAbstractVideoBuffer is not None - assert QtMultimedia.QAudio is not None - assert QtMultimedia.QAudioDeviceInfo is not None - assert QtMultimedia.QAudioInput is not None - assert QtMultimedia.QSound is not None diff --git a/qtpy/tests/test_uic.py b/qtpy/tests/test_uic.py deleted file mode 100644 index 07e3f776..00000000 --- a/qtpy/tests/test_uic.py +++ /dev/null @@ -1,68 +0,0 @@ -import os -import sys -import contextlib - -from qtpy import QtWidgets -from qtpy.QtWidgets import QComboBox -from qtpy.uic import loadUi - - -QCOMBOBOX_SUBCLASS = """ -from qtpy.QtWidgets import QComboBox -class _QComboBoxSubclass(QComboBox): - pass -""" - -@contextlib.contextmanager -def enabled_qcombobox_subclass(tmpdir): - """ - Context manager that sets up a temporary module with a QComboBox subclass - and then removes it once we are done. - """ - - with open(tmpdir.join('qcombobox_subclass.py').strpath, 'w') as f: - f.write(QCOMBOBOX_SUBCLASS) - - sys.path.insert(0, tmpdir.strpath) - - yield - - sys.path.pop(0) - - -def get_qapp(icon_path=None): - """ - Helper function to return a QApplication instance - """ - qapp = QtWidgets.QApplication.instance() - if qapp is None: - qapp = QtWidgets.QApplication(['']) - return qapp - - -def test_load_ui(): - """ - Make sure that the patched loadUi function behaves as expected with a - simple .ui file. - """ - app = get_qapp() - ui = loadUi(os.path.join(os.path.dirname(__file__), 'test.ui')) - assert isinstance(ui.pushButton, QtWidgets.QPushButton) - assert isinstance(ui.comboBox, QComboBox) - - -def test_load_ui_custom_auto(tmpdir): - """ - Test that we can load a .ui file with custom widgets without having to - explicitly specify a dictionary of custom widgets, even in the case of - PySide. - """ - - app = get_qapp() - - with enabled_qcombobox_subclass(tmpdir): - from qcombobox_subclass import _QComboBoxSubclass - ui = loadUi(os.path.join(os.path.dirname(__file__), 'test_custom.ui')) - - assert isinstance(ui.pushButton, QtWidgets.QPushButton) - assert isinstance(ui.comboBox, _QComboBoxSubclass) diff --git a/qtpy/uic.py b/qtpy/uic.py index ff049ff7..6f940530 100644 --- a/qtpy/uic.py +++ b/qtpy/uic.py @@ -1,19 +1,13 @@ -import os +from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6 -from qtpy import PYSIDE, PYQT4, PYQT5 -from qtpy.QtWidgets import QComboBox +if PYQT6: + from PyQt6.uic import * -__all__ = ['loadUi'] +elif PYQT5: + from PyQt5.uic import * -if PYQT5: - - from PyQt5.uic import loadUi - -elif PYQT4: - - from PyQt4.uic import loadUi - -elif PYSIDE: +else: + __all__ = ["loadUi", "loadUiType"] # In PySide, loadUi does not exist, so we define it using QUiLoader, and # then make sure we expose that function. This is adapted from qt-helpers @@ -77,8 +71,34 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. - from PySide.QtCore import QMetaObject - from PySide.QtUiTools import QUiLoader + if PYSIDE6: + from PySide6.QtCore import QMetaObject + from PySide6.QtUiTools import QUiLoader, loadUiType + elif PYSIDE2: + from PySide2.QtCore import QMetaObject + from PySide2.QtUiTools import QUiLoader + + try: + from xml.etree.ElementTree import Element + + from pyside2uic import compileUi + + # Patch UIParser as xml.etree.Elementree.Element.getiterator + # was deprecated since Python 3.2 and removed in Python 3.9 + # https://docs.python.org/3.9/whatsnew/3.9.html#removed + from pyside2uic.uiparser import UIParser + + class ElemPatched(Element): + def getiterator(self, *args, **kwargs): + return self.iter(*args, **kwargs) + + def readResources(self, elem): + return self._readResources(ElemPatched(elem)) + + UIParser._readResources = UIParser.readResources + UIParser.readResources = readResources + except ImportError: + pass class UiLoader(QUiLoader): """ @@ -117,7 +137,7 @@ def __init__(self, baseinstance, customWidgets=None): else: self.customWidgets = customWidgets - def createWidget(self, class_name, parent=None, name=''): + def createWidget(self, class_name, parent=None, name=""): """ Function that is called for each widget defined in ui file, overridden here to populate baseinstance instead. @@ -128,31 +148,36 @@ def createWidget(self, class_name, parent=None, name=''): # instance instead return self.baseinstance - else: + # For some reason, Line is not in the list of available + # widgets, but works fine, so we have to special case it here. + if class_name in self.availableWidgets() or class_name == "Line": + # create a new widget for child widgets + widget = QUiLoader.createWidget( + self, + class_name, + parent, + name, + ) - # For some reason, Line is not in the list of available - # widgets, but works fine, so we have to special case it here. - if class_name in self.availableWidgets() or class_name == 'Line': - # create a new widget for child widgets - widget = QUiLoader.createWidget(self, class_name, parent, name) - - else: - # If not in the list of availableWidgets, must be a custom - # widget. This will raise KeyError if the user has not - # supplied the relevant class_name in the dictionary or if - # customWidgets is empty. - try: - widget = self.customWidgets[class_name](parent) - except KeyError: - raise Exception('No custom widget ' + class_name + ' ' - 'found in customWidgets') - - if self.baseinstance: - # set an attribute for the new child widget on the base - # instance, just like PyQt4.uic.loadUi does. - setattr(self.baseinstance, name, widget) - - return widget + else: + # If not in the list of availableWidgets, must be a custom + # widget. This will raise KeyError if the user has not + # supplied the relevant class_name in the dictionary or if + # customWidgets is empty. + try: + widget = self.customWidgets[class_name](parent) + except KeyError as error: + raise NoCustomWidget( + f"No custom widget {class_name} " + "found in customWidgets", + ) from error + + if self.baseinstance: + # set an attribute for the new child widget on the base + # instance, just like PyQt4.uic.loadUi does. + setattr(self.baseinstance, name, widget) + + return widget def _get_custom_widgets(ui_file): """ @@ -160,7 +185,6 @@ def _get_custom_widgets(ui_file): section, then automatically load all the custom widget classes. """ - import sys import importlib from xml.etree.ElementTree import ElementTree @@ -169,17 +193,16 @@ def _get_custom_widgets(ui_file): ui = etree.parse(ui_file) # Get the customwidgets section - custom_widgets = ui.find('customwidgets') + custom_widgets = ui.find("customwidgets") if custom_widgets is None: return {} custom_widget_classes = {} - for custom_widget in custom_widgets.getchildren(): - - cw_class = custom_widget.find('class').text - cw_header = custom_widget.find('header').text + for custom_widget in list(custom_widgets): + cw_class = custom_widget.find("class").text + cw_header = custom_widget.find("header").text module = importlib.import_module(cw_header) @@ -221,3 +244,44 @@ def loadUi(uifile, baseinstance=None, workingDirectory=None): widget = loader.load(uifile) QMetaObject.connectSlotsByName(widget) return widget + + if PYSIDE2: + + def loadUiType(uifile, from_imports=False): + """Load a .ui file and return the generated form class and + the Qt base class. + + The "loadUiType" command convert the ui file to py code + in-memory first and then execute it in a special frame to + retrieve the form_class. + + Credit: https://stackoverflow.com/a/14195313/15954282 + """ + + import sys + from io import StringIO + from xml.etree.ElementTree import ElementTree + + from . import QtWidgets + + # Parse the UI file + etree = ElementTree() + ui = etree.parse(uifile) + + widget_class = ui.find("widget").get("class") + form_class = ui.find("class").text + + with open(uifile, encoding="utf-8") as fd: + code_stream = StringIO() + frame = {} + + compileUi(fd, code_stream, indent=0, from_imports=from_imports) + pyc = compile(code_stream.getvalue(), "", "exec") + exec(pyc, frame) + + # Fetch the base_class and form class based on their type in the + # xml from designer + form_class = frame["Ui_%s" % form_class] + base_class = getattr(QtWidgets, widget_class) + + return form_class, base_class diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index da092eab..00000000 --- a/setup.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Setup script for qtpy -""" - -import os -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) - -version_ns = {} -with open(os.path.join(here, 'qtpy', '_version.py')) as f: - exec(f.read(), {}, version_ns) - -LONG_DESCRIPTION = """ -.. image:: https://img.shields.io/pypi/v/QtPy.svg - :target: https://pypi.python.org/pypi/QtPy/ - :alt: Latest PyPI version - -.. image:: https://img.shields.io/pypi/dm/QtPy.svg - :target: https://pypi.python.org/pypi/QtPy/ - :alt: Number of PyPI downloads - -QtPy: Abtraction layer for PyQt5/PyQt4/PySide -============================================= - -**QtPy** (pronounced *'cutie pie'*) is a small abstraction layer that lets you -write applications using a single api call to either PyQt or PySide. - -It provides support for PyQt5, PyQt4 and PySide using the PyQt5 layout (where -the QtGui module has been split into QtGui and QtWidgets). - -Basically, you write your code as if you were using PyQt5 but import qt from -``qtpy`` instead of ``PyQt5``. - -- `Issue tracker`_ -- `Changelog`_ - - -Attribution and acknowledgements --------------------------------- - -This project is based on the `pyqode.qt`_ project and the `spyderlib.qt`_ -module from the `spyder`_ project. - -Unlike **pyqode.qt** this is not a namespace package so it is not *tied* -to a particular project, or namespace. - -.. _spyder: https://github.com/spyder-ide/spyder -.. _spyderlib.qt: https://github.com/spyder-ide/spyder/tree/master/spyderlib/qt -.. _pyqode.qt: https://github.com/pyQode/pyqode.qt -.. _Changelog: https://github.com/spyder-ide/qtpy/blob/master/CHANGELOG.md -.. _Issue tracker: https://github.com/spyder-ide/qtpy/issues -""" - -setup( - name='QtPy', - version=version_ns['__version__'], - packages=find_packages(exclude=['contrib', 'docs', 'tests*']), - keywords=["qt PyQt4 PyQt5 PySide"], - url='https://github.com/spyder-ide/qtpy', - license='MIT', - author='Colin Duquesnoy, The Spyder Development Team', - author_email='goanpeca@gmail.com', - maintainer='Gonzalo Peña-Castellanos', - maintainer_email='goanpeca@gmail.com', - description='Provides an abstraction layer on top of the various Qt ' - 'bindings (PyQt5, PyQt4 and PySide) and additional custom ' - 'QWidgets.', - long_description=LONG_DESCRIPTION, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: X11 Applications :: Qt', - 'Environment :: Win32 (MS Windows)', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5'] -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f5c5f9aa --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,86 @@ +import os + +import pytest + + +def pytest_configure(config): + """Configure the test environment.""" + + if "USE_QT_API" in os.environ: + os.environ["QT_API"] = os.environ["USE_QT_API"].lower() + + # We need to import qtpy here to make sure that the API versions get set + # straight away. + import qtpy + + +def pytest_report_header(config): + """Insert a customized header into the test report.""" + + versions = os.linesep + versions += "PyQt5: " + + try: + from PyQt5 import Qt + + versions += f"PyQt: {Qt.PYQT_VERSION_STR} - Qt: {Qt.QT_VERSION_STR}" + except ImportError: + versions += "not installed" + except AttributeError: + versions += "unknown version" + + versions += os.linesep + versions += "PySide2: " + + try: + import PySide2 + from PySide2 import QtCore + + versions += f"PySide: {PySide2.__version__} - Qt: {QtCore.__version__}" + except ImportError: + versions += "not installed" + except AttributeError: + versions += "unknown version" + + versions += os.linesep + versions += "PyQt6: " + + try: + from PyQt6 import QtCore + + versions += ( + f"PyQt: {QtCore.PYQT_VERSION_STR} - Qt: {QtCore.QT_VERSION_STR}" + ) + except ImportError: + versions += "not installed" + except AttributeError: + versions += "unknown version" + + versions += os.linesep + versions += "PySide6: " + + try: + import PySide6 + from PySide6 import QtCore + + versions += f"PySide: {PySide6.__version__} - Qt: {QtCore.__version__}" + except ImportError: + versions += "not installed" + except AttributeError: + versions += "unknown version" + + versions += os.linesep + + return versions + + +@pytest.fixture +def pdf_writer(qtbot): + from pathlib import Path + + from qtpy import QtGui + + output_path = Path("test.pdf") + device = QtGui.QPdfWriter(str(output_path)) + yield device, output_path + output_path.unlink() diff --git a/tests/optional_deps/__init__.py b/tests/optional_deps/__init__.py new file mode 100644 index 00000000..2c957446 --- /dev/null +++ b/tests/optional_deps/__init__.py @@ -0,0 +1,30 @@ +"""Package used for testing the deferred import error mechanism.""" + + +# See https://github.com/spyder-ide/qtpy/pull/387/ + + +from qtpy._utils import getattr_missing_optional_dep + +from .optional_dep import ExampleClass + +_missing_optional_names = {} + + +try: + from .optional_dep import MissingClass +except ImportError as error: + _missing_optional_names["MissingClass"] = { + "name": "optional_dep.MissingClass", + "missing_package": "test_package_please_ignore", + "import_error": error, + } + + +def __getattr__(name): + """Custom getattr to chain and wrap errors due to missing optional deps.""" + raise getattr_missing_optional_dep( + name, + module_name=__name__, + optional_names=_missing_optional_names, + ) diff --git a/tests/optional_deps/optional_dep.py b/tests/optional_deps/optional_dep.py new file mode 100644 index 00000000..818152b6 --- /dev/null +++ b/tests/optional_deps/optional_dep.py @@ -0,0 +1,5 @@ +"""Test module with an optional dependency that may be missing.""" + + +class ExampleClass: + pass diff --git a/qtpy/tests/test.ui b/tests/test.ui similarity index 100% rename from qtpy/tests/test.ui rename to tests/test.ui diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..4b449502 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,156 @@ +"""Test the QtPy CLI.""" + +import subprocess +import sys +import textwrap + +import pytest + +import qtpy + +SUBCOMMANDS = [ + [], + ["mypy-args"], +] + + +@pytest.mark.parametrize( + argnames=["subcommand"], + argvalues=[[subcommand] for subcommand in SUBCOMMANDS], + ids=[" ".join(subcommand) for subcommand in SUBCOMMANDS], +) +def test_cli_help_does_not_fail(subcommand): + subprocess.run( + [sys.executable, "-m", "qtpy", *subcommand, "--help"], + check=True, + ) + + +def test_cli_version(): + output = subprocess.run( + [sys.executable, "-m", "qtpy", "--version"], + capture_output=True, + check=True, + encoding="utf-8", + ) + assert output.stdout.strip().split()[-1] == qtpy.__version__ + + +def test_cli_mypy_args(): + output = subprocess.run( + [sys.executable, "-m", "qtpy", "mypy-args"], + capture_output=True, + check=True, + encoding="utf-8", + ) + + if qtpy.PYQT5: + expected = " ".join( + [ + "--always-true=PYQT5", + "--always-false=PYSIDE2", + "--always-false=PYQT6", + "--always-false=PYSIDE6", + ], + ) + elif qtpy.PYSIDE2: + expected = " ".join( + [ + "--always-false=PYQT5", + "--always-true=PYSIDE2", + "--always-false=PYQT6", + "--always-false=PYSIDE6", + ], + ) + elif qtpy.PYQT6: + expected = " ".join( + [ + "--always-false=PYQT5", + "--always-false=PYSIDE2", + "--always-true=PYQT6", + "--always-false=PYSIDE6", + ], + ) + elif qtpy.PYSIDE6: + expected = " ".join( + [ + "--always-false=PYQT5", + "--always-false=PYSIDE2", + "--always-false=PYQT6", + "--always-true=PYSIDE6", + ], + ) + else: + pytest.fail("No Qt bindings detected") + + assert output.stdout.strip() == expected.strip() + + +def test_cli_pyright_config(): + output = subprocess.run( + [sys.executable, "-m", "qtpy", "pyright-config"], + capture_output=True, + check=True, + encoding="utf-8", + ) + + if qtpy.PYQT5: + expected = textwrap.dedent( + """ + pyrightconfig.json: + {"defineConstant": {"PYQT5": true, "PYSIDE2": false, "PYQT6": false, "PYSIDE6": false}} + + pyproject.toml: + [tool.pyright.defineConstant] + PYQT5 = true + PYSIDE2 = false + PYQT6 = false + PYSIDE6 = false + """, + ) + elif qtpy.PYSIDE2: + expected = textwrap.dedent( + """ + pyrightconfig.json: + {"defineConstant": {"PYQT5": false, "PYSIDE2": true, "PYQT6": false, "PYSIDE6": false}} + + pyproject.toml: + [tool.pyright.defineConstant] + PYQT5 = false + PYSIDE2 = true + PYQT6 = false + PYSIDE6 = false + """, + ) + elif qtpy.PYQT6: + expected = textwrap.dedent( + """ + pyrightconfig.json: + {"defineConstant": {"PYQT5": false, "PYSIDE2": false, "PYQT6": true, "PYSIDE6": false}} + + pyproject.toml: + [tool.pyright.defineConstant] + PYQT5 = false + PYSIDE2 = false + PYQT6 = true + PYSIDE6 = false + """, + ) + elif qtpy.PYSIDE6: + expected = textwrap.dedent( + """ + pyrightconfig.json: + {"defineConstant": {"PYQT5": false, "PYSIDE2": false, "PYQT6": false, "PYSIDE6": true}} + + pyproject.toml: + [tool.pyright.defineConstant] + PYQT5 = false + PYSIDE2 = false + PYQT6 = false + PYSIDE6 = true + """, + ) + else: + pytest.fail("No valid API to test") + + assert output.stdout.strip() == expected.strip() diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 00000000..57f6fd00 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,53 @@ +"""Test the compat module.""" + +import sys + +import pytest +from packaging import version + +from qtpy import PYQT5, PYQT_VERSION, QtWidgets, compat +from qtpy.QtCore import QSize +from qtpy.QtGui import QImage +from tests.utils import not_using_conda + + +@pytest.mark.skipif( + ( + (sys.version_info.major == 3 and sys.version_info.minor == 7) + and sys.platform.startswith("win") + and not not_using_conda() + ), + reason="sip not included in Python3.7 on Windows", +) +def test_isalive(qtbot): + """Test compat.isalive""" + test_widget = QtWidgets.QWidget() + assert compat.isalive(test_widget) is True + with qtbot.waitSignal(test_widget.destroyed): + test_widget.deleteLater() + assert compat.isalive(test_widget) is False + + +def test_getenumasint(): + """Test compat.getenumasint""" + if PYQT5 and version.parse(PYQT_VERSION) <= version.parse("5.9.2"): + assert compat.getenumasint(QtWidgets.QSizePolicy.Maximum) == 4 + else: + assert compat.getenumasint(QtWidgets.QSizePolicy.Policy.Maximum) == 4 + assert compat.getenumasint(5) == 5 + + +def test_getenumfromint(): + """Test compat.getenumfromint""" + enum_value = compat.getenumfromint(QtWidgets.QSizePolicy.Policy, 7) + if PYQT5 and version.parse(PYQT_VERSION) <= version.parse("5.9.2"): + assert enum_value == QtWidgets.QSizePolicy.Expanding + else: + assert enum_value == QtWidgets.QSizePolicy.Policy.Expanding + + +def test_getimagebytes(qtbot): + """Test compat.getimagebytes""" + image = QImage(QSize(100, 100), QImage.Format_RGB32) + _bytes = compat.getimagebytes(image) + assert len(_bytes) == 100 * 100 * 4 diff --git a/qtpy/tests/test_custom.ui b/tests/test_custom.ui similarity index 100% rename from qtpy/tests/test_custom.ui rename to tests/test_custom.ui diff --git a/tests/test_macos_checks.py b/tests/test_macos_checks.py new file mode 100644 index 00000000..e9698b56 --- /dev/null +++ b/tests/test_macos_checks.py @@ -0,0 +1,106 @@ +import contextlib +import platform +import sys +from unittest import mock + +import pytest + +from qtpy import PYQT5, PYSIDE2 + + +@pytest.mark.skipif(not PYQT5, reason="Targeted to PyQt5") +@mock.patch.object(platform, "mac_ver") +def test_qt59_exception(mac_ver, monkeypatch): + # Remove qtpy to reimport it again + with contextlib.suppress(KeyError): + del sys.modules["qtpy"] + + # Patch stdlib to emulate a macOS system + monkeypatch.setattr("sys.platform", "darwin") + mac_ver.return_value = ("10.9.2",) + + # Patch Qt version + monkeypatch.setattr("PyQt5.QtCore.QT_VERSION_STR", "5.9.1") + + # This should raise an Exception + with pytest.raises(Exception) as e: + import qtpy + + assert "10.10" in str(e.value) + assert "5.9" in str(e.value) + + +@pytest.mark.skipif(not PYQT5, reason="Targeted to PyQt5") +@mock.patch.object(platform, "mac_ver") +def test_qt59_no_exception(mac_ver, monkeypatch): + # Remove qtpy to reimport it again + with contextlib.suppress(KeyError): + del sys.modules["qtpy"] + + # Patch stdlib to emulate a macOS system + monkeypatch.setattr("sys.platform", "darwin") + mac_ver.return_value = ("10.10.1",) + + # Patch Qt version + monkeypatch.setattr("PyQt5.QtCore.QT_VERSION_STR", "5.9.5") + + # This should not raise an Exception + try: + import qtpy + except Exception: # noqa: BLE001 + pytest.fail("Error!") + + +@pytest.mark.skipif( + not (PYQT5 or PYSIDE2), + reason="Targeted to PyQt5 or PySide2", +) +@mock.patch.object(platform, "mac_ver") +def test_qt511_exception(mac_ver, monkeypatch): + # Remove qtpy to reimport it again + with contextlib.suppress(KeyError): + del sys.modules["qtpy"] + + # Patch stdlib to emulate a macOS system + monkeypatch.setattr("sys.platform", "darwin") + mac_ver.return_value = ("10.10.3",) + + # Patch Qt version + if PYQT5: + monkeypatch.setattr("PyQt5.QtCore.QT_VERSION_STR", "5.11.1") + else: + monkeypatch.setattr("PySide2.QtCore.__version__", "5.11.1") + + # This should raise an Exception + with pytest.raises(Exception) as e: + import qtpy + + assert "10.11" in str(e.value) + assert "5.11" in str(e.value) + + +@pytest.mark.skipif( + not (PYQT5 or PYSIDE2), + reason="Targeted to PyQt5 or PySide2", +) +@mock.patch.object(platform, "mac_ver") +def test_qt511_no_exception(mac_ver, monkeypatch): + # Remove qtpy to reimport it again + with contextlib.suppress(KeyError): + del sys.modules["qtpy"] + + # Patch stdlib to emulate a macOS system + monkeypatch.setattr("sys.platform", "darwin") + mac_ver.return_value = ("10.13.2",) + + # Patch Qt version + if PYQT5: + monkeypatch.setattr("PyQt5.QtCore.QT_VERSION_STR", "5.11.1") + else: + monkeypatch.setattr("PySide2.QtCore.__version__", "5.11.1") + + # This should not raise an Exception + try: + import qtpy + except Exception: # noqa: BLE001 + pytest.fail("Error!") diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 00000000..27d3d545 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,168 @@ +import contextlib +import os +import subprocess +import sys + +import pytest + +from qtpy import ( + API_NAMES, + QtCore, + QtGui, + QtWidgets, + _parse_version, + _parse_version_internal, +) +from tests.utils import pytest_importorskip + +with contextlib.suppress(Exception): + # removed in qt 6.0 + from qtpy import QtWebEngineWidgets + + +def assert_pyside2(): + """ + Make sure that we are using PySide + """ + import PySide2 + + assert QtCore.QEvent is PySide2.QtCore.QEvent + assert QtGui.QPainter is PySide2.QtGui.QPainter + assert QtWidgets.QWidget is PySide2.QtWidgets.QWidget + assert ( + QtWebEngineWidgets.QWebEnginePage + is PySide2.QtWebEngineWidgets.QWebEnginePage + ) + assert os.environ["QT_API"] == "pyside2" + + +def assert_pyside6(): + """ + Make sure that we are using PySide + """ + import PySide6 + + assert QtCore.QEvent is PySide6.QtCore.QEvent + assert QtGui.QPainter is PySide6.QtGui.QPainter + assert QtWidgets.QWidget is PySide6.QtWidgets.QWidget + # Only valid for qt>=6.2 + # assert QtWebEngineWidgets.QWebEnginePage is PySide6.QtWebEngineCore.QWebEnginePage + assert os.environ["QT_API"] == "pyside6" + + +def assert_pyqt5(): + """ + Make sure that we are using PyQt5 + """ + import PyQt5 + + assert QtCore.QEvent is PyQt5.QtCore.QEvent + assert QtGui.QPainter is PyQt5.QtGui.QPainter + assert QtWidgets.QWidget is PyQt5.QtWidgets.QWidget + assert os.environ["QT_API"] == "pyqt5" + + +def assert_pyqt6(): + """ + Make sure that we are using PyQt6 + """ + import PyQt6 + + assert QtCore.QEvent is PyQt6.QtCore.QEvent + assert QtGui.QPainter is PyQt6.QtGui.QPainter + assert QtWidgets.QWidget is PyQt6.QtWidgets.QWidget + assert os.environ["QT_API"] == "pyqt6" + + +def test_qt_api(): + """ + If QT_API is specified, we check that the correct Qt wrapper was used + """ + + QT_API = os.environ.get("QT_API", "").lower() + + if QT_API == "pyqt5": + assert_pyqt5() + elif QT_API == "pyside2": + assert_pyside2() + elif QT_API == "pyqt6": + assert_pyqt6() + elif QT_API == "pyside6": + assert_pyside6() + else: + # If the tests are run locally, USE_QT_API and QT_API may not be + # defined, but we still want to make sure qtpy is behaving sensibly. + # We should then be loading, in order of decreasing preference, PyQt5, + # PySide2, PyQt6 and PySide6. + try: + import PyQt5 + except ImportError: + try: + import PySide2 + except ImportError: + try: + import PyQt6 + except ImportError: + import PySide6 + + assert_pyside6() + else: + assert_pyqt6() + else: + assert_pyside2() + else: + assert_pyqt5() + + +@pytest.mark.parametrize("api", API_NAMES.values()) +def test_qt_api_environ(api): + """ + If no QT_API is specified but some Qt is imported, ensure QT_API is set properly. + """ + mod = f"{api}.QtCore" + pytest_importorskip(mod, reason=f"Requires {api}") + # clean env + env = os.environ.copy() + for key in ("QT_API", "USE_QT_API"): + if key in env: + del env[key] + cmd = f""" +import {mod} +from qtpy import API +import os +print(API) +print(os.environ['QT_API']) +""" + output = subprocess.check_output([sys.executable, "-c", cmd], env=env) + got_api, env_qt_api = output.strip().decode("utf-8").splitlines() + assert got_api == api.lower() + assert env_qt_api == api.lower() + # Also ensure we raise a nice error + env["QT_API"] = "bad" + cmd = """ +try: + import qtpy +except ValueError as exc: + assert 'Specified QT_API' in str(exc), str(exc) +else: + raise AssertionError('QtPy imported despite bad QT_API') +""" + subprocess.check_call([sys.executable, "-Oc", cmd], env=env) + + +@pytest.mark.parametrize( + "first,second", + [("1.2.3", "1.2.3.1"), ("1.2.3", "1.10.0")], +) +def test_parse_version(first, second): + """Verify the behavior of _parse_version()""" + assert _parse_version(first) < _parse_version(second) + + +@pytest.mark.parametrize( + "value,expect", + [("1.2.3", (1, 2, 3)), ("1.x.3", (1, 0, 3))], +) +def test_parse_version_internal(value, expect): + """Verify the behavior of _parse_version_internal()""" + assert _parse_version_internal(value) == expect diff --git a/tests/test_missing_optional_deps.py b/tests/test_missing_optional_deps.py new file mode 100644 index 00000000..e1ca82b0 --- /dev/null +++ b/tests/test_missing_optional_deps.py @@ -0,0 +1,22 @@ +"""Test the deferred import error mechanism""" + + +# See https://github.com/spyder-ide/qtpy/pull/387/ + + +import pytest + +from qtpy import QtModuleNotInstalledError + + +def test_missing_optional_deps(): + """Test importing a module that uses the deferred import error mechanism""" + from . import optional_deps + + assert optional_deps.ExampleClass is not None + + with pytest.raises(QtModuleNotInstalledError) as excinfo: + from .optional_deps import MissingClass + + msg = "The optional_dep.MissingClass module was not found. It must be installed separately as test_package_please_ignore." + assert msg == str(excinfo.value) diff --git a/tests/test_qdesktopservice_split.py b/tests/test_qdesktopservice_split.py new file mode 100644 index 00000000..98192884 --- /dev/null +++ b/tests/test_qdesktopservice_split.py @@ -0,0 +1,22 @@ +"""Test QDesktopServices split in Qt5.""" + + +import pytest + + +def test_qstandarpath(): + """Test the qtpy.QStandardPaths namespace""" + from qtpy.QtCore import QStandardPaths + + assert QStandardPaths.StandardLocation is not None + + # Attributes from QDesktopServices shouldn't be in QStandardPaths + with pytest.raises(AttributeError): + QStandardPaths.setUrlHandler # noqa: B018 + + +def test_qdesktopservice(): + """Test the qtpy.QDesktopServices namespace""" + from qtpy.QtGui import QDesktopServices + + assert QDesktopServices.setUrlHandler is not None diff --git a/tests/test_qsci.py b/tests/test_qsci.py new file mode 100644 index 00000000..b506be17 --- /dev/null +++ b/tests/test_qsci.py @@ -0,0 +1,68 @@ +"""Test Qsci.""" + +import pytest + +from qtpy import PYSIDE2, PYSIDE6 +from tests.utils import pytest_importorskip, using_conda + + +@pytest.mark.skipif( + PYSIDE2 or PYSIDE6 or using_conda(), + reason="Qsci bindings not available under PySide 2/6 and conda installations", +) +def test_qsci(): + """Test the qtpy.Qsci namespace""" + Qsci = pytest_importorskip("qtpy.Qsci") + assert Qsci.QSCINTILLA_VERSION is not None + assert Qsci.QSCINTILLA_VERSION_STR is not None + assert Qsci.QsciAPIs is not None + assert Qsci.QsciAbstractAPIs is not None + assert Qsci.QsciCommand is not None + assert Qsci.QsciCommandSet is not None + assert Qsci.QsciDocument is not None + assert Qsci.QsciLexer is not None + assert Qsci.QsciLexerAVS is not None + assert Qsci.QsciLexerBash is not None + assert Qsci.QsciLexerBatch is not None + assert Qsci.QsciLexerCMake is not None + assert Qsci.QsciLexerCPP is not None + assert Qsci.QsciLexerCSS is not None + assert Qsci.QsciLexerCSharp is not None + assert Qsci.QsciLexerCoffeeScript is not None + assert Qsci.QsciLexerCustom is not None + assert Qsci.QsciLexerD is not None + assert Qsci.QsciLexerDiff is not None + assert Qsci.QsciLexerFortran is not None + assert Qsci.QsciLexerFortran77 is not None + assert Qsci.QsciLexerHTML is not None + assert Qsci.QsciLexerIDL is not None + assert Qsci.QsciLexerJSON is not None + assert Qsci.QsciLexerJava is not None + assert Qsci.QsciLexerJavaScript is not None + assert Qsci.QsciLexerLua is not None + assert Qsci.QsciLexerMakefile is not None + assert Qsci.QsciLexerMarkdown is not None + assert Qsci.QsciLexerMatlab is not None + assert Qsci.QsciLexerOctave is not None + assert Qsci.QsciLexerPO is not None + assert Qsci.QsciLexerPOV is not None + assert Qsci.QsciLexerPascal is not None + assert Qsci.QsciLexerPerl is not None + assert Qsci.QsciLexerPostScript is not None + assert Qsci.QsciLexerProperties is not None + assert Qsci.QsciLexerPython is not None + assert Qsci.QsciLexerRuby is not None + assert Qsci.QsciLexerSQL is not None + assert Qsci.QsciLexerSpice is not None + assert Qsci.QsciLexerTCL is not None + assert Qsci.QsciLexerTeX is not None + assert Qsci.QsciLexerVHDL is not None + assert Qsci.QsciLexerVerilog is not None + assert Qsci.QsciLexerXML is not None + assert Qsci.QsciLexerYAML is not None + assert Qsci.QsciMacro is not None + assert Qsci.QsciPrinter is not None + assert Qsci.QsciScintilla is not None + assert Qsci.QsciScintillaBase is not None + assert Qsci.QsciStyle is not None + assert Qsci.QsciStyledText is not None diff --git a/tests/test_qt3danimation.py b/tests/test_qt3danimation.py new file mode 100644 index 00000000..53cccf55 --- /dev/null +++ b/tests/test_qt3danimation.py @@ -0,0 +1,22 @@ +from tests.utils import pytest_importorskip + + +def test_qt3danimation(): + """Test the qtpy.Qt3DAnimation namespace""" + Qt3DAnimation = pytest_importorskip("qtpy.Qt3DAnimation") + + assert Qt3DAnimation.QAnimationController is not None + assert Qt3DAnimation.QAdditiveClipBlend is not None + assert Qt3DAnimation.QAbstractClipBlendNode is not None + assert Qt3DAnimation.QAbstractAnimation is not None + assert Qt3DAnimation.QKeyframeAnimation is not None + assert Qt3DAnimation.QAbstractAnimationClip is not None + assert Qt3DAnimation.QAbstractClipAnimator is not None + assert Qt3DAnimation.QClipAnimator is not None + assert Qt3DAnimation.QAnimationGroup is not None + assert Qt3DAnimation.QLerpClipBlend is not None + assert Qt3DAnimation.QMorphingAnimation is not None + assert Qt3DAnimation.QAnimationAspect is not None + assert Qt3DAnimation.QVertexBlendAnimation is not None + assert Qt3DAnimation.QBlendedClipAnimator is not None + assert Qt3DAnimation.QMorphTarget is not None diff --git a/tests/test_qt3dcore.py b/tests/test_qt3dcore.py new file mode 100644 index 00000000..f63cd82c --- /dev/null +++ b/tests/test_qt3dcore.py @@ -0,0 +1,46 @@ +import pytest + +from qtpy import PYQT6, PYSIDE6 +from tests.utils import pytest_importorskip + + +@pytest.mark.skipif(PYQT6, reason="Not complete in PyQt6") +@pytest.mark.skipif(PYSIDE6, reason="Not complete in PySide6") +def test_qt3dcore(): + """Test the qtpy.Qt3DCore namespace""" + Qt3DCore = pytest_importorskip("qtpy.Qt3DCore") + + assert Qt3DCore.QPropertyValueAddedChange is not None + assert Qt3DCore.QSkeletonLoader is not None + assert Qt3DCore.QPropertyNodeRemovedChange is not None + assert Qt3DCore.QPropertyUpdatedChange is not None + assert Qt3DCore.QAspectEngine is not None + assert Qt3DCore.QPropertyValueAddedChangeBase is not None + assert Qt3DCore.QStaticPropertyValueRemovedChangeBase is not None + assert Qt3DCore.QPropertyNodeAddedChange is not None + assert Qt3DCore.QDynamicPropertyUpdatedChange is not None + assert Qt3DCore.QStaticPropertyUpdatedChangeBase is not None + assert Qt3DCore.ChangeFlags is not None + assert Qt3DCore.QAbstractAspect is not None + assert Qt3DCore.QBackendNode is not None + assert Qt3DCore.QTransform is not None + assert Qt3DCore.QPropertyUpdatedChangeBase is not None + assert Qt3DCore.QNodeId is not None + assert Qt3DCore.QJoint is not None + assert Qt3DCore.QSceneChange is not None + assert Qt3DCore.QNodeIdTypePair is not None + assert Qt3DCore.QAbstractSkeleton is not None + assert Qt3DCore.QComponentRemovedChange is not None + assert Qt3DCore.QComponent is not None + assert Qt3DCore.QEntity is not None + assert Qt3DCore.QNodeCommand is not None + assert Qt3DCore.QNode is not None + assert Qt3DCore.QPropertyValueRemovedChange is not None + assert Qt3DCore.QPropertyValueRemovedChangeBase is not None + assert Qt3DCore.QComponentAddedChange is not None + assert Qt3DCore.QNodeCreatedChangeBase is not None + assert Qt3DCore.QNodeDestroyedChange is not None + assert Qt3DCore.QArmature is not None + assert Qt3DCore.QStaticPropertyValueAddedChangeBase is not None + assert Qt3DCore.ChangeFlag is not None + assert Qt3DCore.QSkeleton is not None diff --git a/tests/test_qt3dextras.py b/tests/test_qt3dextras.py new file mode 100644 index 00000000..104103ae --- /dev/null +++ b/tests/test_qt3dextras.py @@ -0,0 +1,44 @@ +from tests.utils import pytest_importorskip + + +def test_qt3dextras(): + """Test the qtpy.Qt3DExtras namespace""" + Qt3DExtras = pytest_importorskip("qtpy.Qt3DExtras") + + assert Qt3DExtras.QTextureMaterial is not None + assert Qt3DExtras.QPhongAlphaMaterial is not None + assert Qt3DExtras.QOrbitCameraController is not None + assert Qt3DExtras.QAbstractSpriteSheet is not None + assert Qt3DExtras.QNormalDiffuseMapMaterial is not None + assert Qt3DExtras.QDiffuseSpecularMaterial is not None + assert Qt3DExtras.QSphereGeometry is not None + assert Qt3DExtras.QCuboidGeometry is not None + assert Qt3DExtras.QForwardRenderer is not None + assert Qt3DExtras.QPhongMaterial is not None + assert Qt3DExtras.QSpriteGrid is not None + assert Qt3DExtras.QDiffuseMapMaterial is not None + assert Qt3DExtras.QConeGeometry is not None + assert Qt3DExtras.QSpriteSheetItem is not None + assert Qt3DExtras.QPlaneGeometry is not None + assert Qt3DExtras.QSphereMesh is not None + assert Qt3DExtras.QNormalDiffuseSpecularMapMaterial is not None + assert Qt3DExtras.QCuboidMesh is not None + assert Qt3DExtras.QGoochMaterial is not None + assert Qt3DExtras.QText2DEntity is not None + assert Qt3DExtras.QTorusMesh is not None + assert Qt3DExtras.Qt3DWindow is not None + assert Qt3DExtras.QPerVertexColorMaterial is not None + assert Qt3DExtras.QExtrudedTextGeometry is not None + assert Qt3DExtras.QSkyboxEntity is not None + assert Qt3DExtras.QAbstractCameraController is not None + assert Qt3DExtras.QExtrudedTextMesh is not None + assert Qt3DExtras.QCylinderGeometry is not None + assert Qt3DExtras.QTorusGeometry is not None + assert Qt3DExtras.QMorphPhongMaterial is not None + assert Qt3DExtras.QPlaneMesh is not None + assert Qt3DExtras.QDiffuseSpecularMapMaterial is not None + assert Qt3DExtras.QSpriteSheet is not None + assert Qt3DExtras.QConeMesh is not None + assert Qt3DExtras.QFirstPersonCameraController is not None + assert Qt3DExtras.QMetalRoughMaterial is not None + assert Qt3DExtras.QCylinderMesh is not None diff --git a/tests/test_qt3dinput.py b/tests/test_qt3dinput.py new file mode 100644 index 00000000..03c2915d --- /dev/null +++ b/tests/test_qt3dinput.py @@ -0,0 +1,29 @@ +from tests.utils import pytest_importorskip + + +def test_qt3dinput(): + """Test the qtpy.Qt3DInput namespace""" + Qt3DInput = pytest_importorskip("qtpy.Qt3DInput") + + assert Qt3DInput.QAxisAccumulator is not None + assert Qt3DInput.QInputSettings is not None + assert Qt3DInput.QAnalogAxisInput is not None + assert Qt3DInput.QAbstractAxisInput is not None + assert Qt3DInput.QMouseHandler is not None + assert Qt3DInput.QButtonAxisInput is not None + assert Qt3DInput.QInputSequence is not None + assert Qt3DInput.QWheelEvent is not None + assert Qt3DInput.QActionInput is not None + assert Qt3DInput.QKeyboardDevice is not None + assert Qt3DInput.QMouseDevice is not None + assert Qt3DInput.QAxis is not None + assert Qt3DInput.QInputChord is not None + assert Qt3DInput.QMouseEvent is not None + assert Qt3DInput.QKeyboardHandler is not None + assert Qt3DInput.QKeyEvent is not None + assert Qt3DInput.QAbstractActionInput is not None + assert Qt3DInput.QInputAspect is not None + assert Qt3DInput.QLogicalDevice is not None + assert Qt3DInput.QAction is not None + assert Qt3DInput.QAbstractPhysicalDevice is not None + assert Qt3DInput.QAxisSetting is not None diff --git a/tests/test_qt3dlogic.py b/tests/test_qt3dlogic.py new file mode 100644 index 00000000..6d35772c --- /dev/null +++ b/tests/test_qt3dlogic.py @@ -0,0 +1,9 @@ +from tests.utils import pytest_importorskip + + +def test_qt3dlogic(): + """Test the qtpy.Qt3DLogic namespace""" + Qt3DLogic = pytest_importorskip("qtpy.Qt3DLogic") + + assert Qt3DLogic.QLogicAspect is not None + assert Qt3DLogic.QFrameAction is not None diff --git a/tests/test_qt3drender.py b/tests/test_qt3drender.py new file mode 100644 index 00000000..4152d970 --- /dev/null +++ b/tests/test_qt3drender.py @@ -0,0 +1,121 @@ +import pytest + +from qtpy import PYQT6, PYSIDE6 +from tests.utils import pytest_importorskip + + +@pytest.mark.skipif(PYQT6, reason="Not complete in PyQt6") +@pytest.mark.skipif(PYSIDE6, reason="Not complete in PySide6") +def test_qt3drender(): + """Test the qtpy.Qt3DRender namespace""" + Qt3DRender = pytest_importorskip("qtpy.Qt3DRender") + + assert Qt3DRender.QPointSize is not None + assert Qt3DRender.QFrustumCulling is not None + assert Qt3DRender.QPickPointEvent is not None + assert Qt3DRender.QRenderPassFilter is not None + assert Qt3DRender.QMesh is not None + assert Qt3DRender.QRayCaster is not None + assert Qt3DRender.QStencilMask is not None + assert Qt3DRender.QPickLineEvent is not None + assert Qt3DRender.QPickTriangleEvent is not None + assert Qt3DRender.QRenderState is not None + assert Qt3DRender.QTextureWrapMode is not None + assert Qt3DRender.QRenderPass is not None + assert Qt3DRender.QGeometryRenderer is not None + assert Qt3DRender.QAttribute is not None + assert Qt3DRender.QStencilOperation is not None + assert Qt3DRender.QScissorTest is not None + assert Qt3DRender.QTextureCubeMapArray is not None + assert Qt3DRender.QRenderTarget is not None + assert Qt3DRender.QStencilTest is not None + assert Qt3DRender.QTextureData is not None + assert Qt3DRender.QBuffer is not None + assert Qt3DRender.QLineWidth is not None + assert Qt3DRender.QLayer is not None + assert Qt3DRender.QTextureRectangle is not None + assert Qt3DRender.QRenderTargetSelector is not None + assert Qt3DRender.QPickingSettings is not None + assert Qt3DRender.QCullFace is not None + assert Qt3DRender.QAbstractFunctor is not None + assert Qt3DRender.PropertyReaderInterface is not None + assert Qt3DRender.QMaterial is not None + assert Qt3DRender.QAlphaCoverage is not None + assert Qt3DRender.QClearBuffers is not None + assert Qt3DRender.QAlphaTest is not None + assert Qt3DRender.QStencilOperationArguments is not None + assert Qt3DRender.QTexture2DMultisample is not None + assert Qt3DRender.QLevelOfDetailSwitch is not None + assert Qt3DRender.QRenderStateSet is not None + assert Qt3DRender.QViewport is not None + assert Qt3DRender.QObjectPicker is not None + assert Qt3DRender.QPolygonOffset is not None + assert Qt3DRender.QRenderSettings is not None + assert Qt3DRender.QFrontFace is not None + assert Qt3DRender.QTexture3D is not None + assert Qt3DRender.QTextureBuffer is not None + assert Qt3DRender.QTechniqueFilter is not None + assert Qt3DRender.QLayerFilter is not None + assert Qt3DRender.QFilterKey is not None + assert Qt3DRender.QRenderSurfaceSelector is not None + assert Qt3DRender.QEnvironmentLight is not None + assert Qt3DRender.QMemoryBarrier is not None + assert Qt3DRender.QNoDepthMask is not None + assert Qt3DRender.QBlitFramebuffer is not None + assert Qt3DRender.QGraphicsApiFilter is not None + assert Qt3DRender.QAbstractTexture is not None + assert Qt3DRender.QRenderCaptureReply is not None + assert Qt3DRender.QAbstractLight is not None + assert Qt3DRender.QAbstractRayCaster is not None + assert Qt3DRender.QDirectionalLight is not None + assert Qt3DRender.QDispatchCompute is not None + assert Qt3DRender.QBufferDataGenerator is not None + assert Qt3DRender.QPointLight is not None + assert Qt3DRender.QStencilTestArguments is not None + assert Qt3DRender.QTexture1D is not None + assert Qt3DRender.QCameraSelector is not None + assert Qt3DRender.QProximityFilter is not None + assert Qt3DRender.QTexture1DArray is not None + assert Qt3DRender.QBlendEquation is not None + assert Qt3DRender.QTextureImageDataGenerator is not None + assert Qt3DRender.QSpotLight is not None + assert Qt3DRender.QEffect is not None + assert Qt3DRender.QSeamlessCubemap is not None + assert Qt3DRender.QTexture2DMultisampleArray is not None + assert Qt3DRender.QComputeCommand is not None + assert Qt3DRender.QFrameGraphNode is not None + assert Qt3DRender.QSortPolicy is not None + assert Qt3DRender.QTextureImageData is not None + assert Qt3DRender.QCamera is not None + assert Qt3DRender.QGeometry is not None + assert Qt3DRender.QScreenRayCaster is not None + assert Qt3DRender.QClipPlane is not None + assert Qt3DRender.QMultiSampleAntiAliasing is not None + assert Qt3DRender.QRayCasterHit is not None + assert Qt3DRender.QAbstractTextureImage is not None + assert Qt3DRender.QNoDraw is not None + assert Qt3DRender.QPickEvent is not None + assert Qt3DRender.QRenderCapture is not None + assert Qt3DRender.QDepthTest is not None + assert Qt3DRender.QParameter is not None + assert Qt3DRender.QLevelOfDetail is not None + assert Qt3DRender.QGeometryFactory is not None + assert Qt3DRender.QTexture2D is not None + assert Qt3DRender.QRenderAspect is not None + assert Qt3DRender.QPaintedTextureImage is not None + assert Qt3DRender.QDithering is not None + assert Qt3DRender.QTextureGenerator is not None + assert Qt3DRender.QBlendEquationArguments is not None + assert Qt3DRender.QLevelOfDetailBoundingSphere is not None + assert Qt3DRender.QColorMask is not None + assert Qt3DRender.QSceneLoader is not None + assert Qt3DRender.QTextureLoader is not None + assert Qt3DRender.QShaderProgram is not None + assert Qt3DRender.QTextureCubeMap is not None + assert Qt3DRender.QTexture2DArray is not None + assert Qt3DRender.QTextureImage is not None + assert Qt3DRender.QCameraLens is not None + assert Qt3DRender.QRenderTargetOutput is not None + assert Qt3DRender.QShaderProgramBuilder is not None + assert Qt3DRender.QTechnique is not None + assert Qt3DRender.QShaderData is not None diff --git a/tests/test_qtaxcontainer.py b/tests/test_qtaxcontainer.py new file mode 100644 index 00000000..d3710379 --- /dev/null +++ b/tests/test_qtaxcontainer.py @@ -0,0 +1,9 @@ +from tests.utils import pytest_importorskip + + +def test_qtaxcontainer(): + """Test the qtpy.QtAxContainer namespace""" + QtAxContainer = pytest_importorskip("qtpy.QtAxContainer") + + assert QtAxContainer.QAxSelect is not None + assert QtAxContainer.QAxWidget is not None diff --git a/tests/test_qtbluetooth.py b/tests/test_qtbluetooth.py new file mode 100644 index 00000000..87e1cd4b --- /dev/null +++ b/tests/test_qtbluetooth.py @@ -0,0 +1,14 @@ +from tests.utils import pytest_importorskip + + +def test_qtbluetooth(): + """Test the qtpy.QtBluetooth namespace""" + QtBluetooth = pytest_importorskip("qtpy.QtBluetooth") + + assert QtBluetooth.QBluetooth is not None + assert QtBluetooth.QBluetoothDeviceInfo is not None + assert QtBluetooth.QBluetoothServer is not None + assert QtBluetooth.QBluetoothSocket is not None + assert QtBluetooth.QBluetoothAddress is not None + assert QtBluetooth.QBluetoothUuid is not None + assert QtBluetooth.QBluetoothServiceDiscoveryAgent is not None diff --git a/tests/test_qtcharts.py b/tests/test_qtcharts.py new file mode 100644 index 00000000..03c2f8cb --- /dev/null +++ b/tests/test_qtcharts.py @@ -0,0 +1,16 @@ +import pytest + +from qtpy import PYSIDE2, PYSIDE6 +from tests.utils import pytest_importorskip + + +@pytest.mark.skipif( + not (PYSIDE2 or PYSIDE6), + reason="Only available by default in PySide", +) +def test_qtcharts(): + """Test the qtpy.QtCharts namespace""" + QtCharts = pytest_importorskip("qtpy.QtCharts") + + assert QtCharts.QChart is not None + assert QtCharts.QtCharts.QChart is not None diff --git a/tests/test_qtconcurrent.py b/tests/test_qtconcurrent.py new file mode 100644 index 00000000..ba92fc8b --- /dev/null +++ b/tests/test_qtconcurrent.py @@ -0,0 +1,15 @@ +from qtpy import PYSIDE2, PYSIDE_VERSION, _parse_version +from tests.utils import pytest_importorskip + + +def test_qtconcurrent(): + """Test the qtpy.QtConcurrent namespace""" + QtConcurrent = pytest_importorskip("qtpy.QtConcurrent") + + assert QtConcurrent.QtConcurrent is not None + + if PYSIDE2 and _parse_version(PYSIDE_VERSION) >= _parse_version("5.15.2"): + assert QtConcurrent.QFutureQString is not None + assert QtConcurrent.QFutureVoid is not None + assert QtConcurrent.QFutureWatcherQString is not None + assert QtConcurrent.QFutureWatcherVoid is not None diff --git a/tests/test_qtcore.py b/tests/test_qtcore.py new file mode 100644 index 00000000..55fbb9e0 --- /dev/null +++ b/tests/test_qtcore.py @@ -0,0 +1,214 @@ +"""Test QtCore.""" + +import enum +import sys +from datetime import date, datetime, time + +import pytest + +from qtpy import ( + PYQT5, + PYQT6, + PYQT_VERSION, + PYSIDE2, + PYSIDE_VERSION, + QtCore, + _parse_version, +) + +_now = datetime.now() +# Make integer milliseconds; `floor` here, don't `round`! +NOW = _now.replace(microsecond=(_now.microsecond // 1000 * 1000)) + + +def test_qtmsghandler(): + """Test qtpy.QtMsgHandler""" + assert QtCore.qInstallMessageHandler is not None + + +@pytest.mark.parametrize("method", ["toPython", "toPyDateTime"]) +def test_QDateTime_toPython_and_toPyDateTime(method): + """Test `QDateTime.toPython` and `QDateTime.toPyDateTime`""" + q_datetime = QtCore.QDateTime(NOW) + py_datetime = getattr(q_datetime, method)() + assert isinstance(py_datetime, datetime) + assert py_datetime == NOW + + +@pytest.mark.parametrize("method", ["toPython", "toPyDate"]) +def test_QDate_toPython_and_toPyDate(method): + """Test `QDate.toPython` and `QDate.toPyDate`""" + q_date = QtCore.QDateTime(NOW).date() + py_date = getattr(q_date, method)() + assert isinstance(py_date, date) + assert py_date == NOW.date() + + +@pytest.mark.parametrize("method", ["toPython", "toPyTime"]) +def test_QTime_toPython_and_toPyTime(method): + """Test `QTime.toPython` and `QTime.toPyTime`""" + q_time = QtCore.QDateTime(NOW).time() + py_time = getattr(q_time, method)() + assert isinstance(py_time, time) + assert py_time == NOW.time() + + +def test_qeventloop_exec(qtbot): + """Test `QEventLoop.exec_` and `QEventLoop.exec`""" + assert QtCore.QEventLoop.exec_ is not None + assert QtCore.QEventLoop.exec is not None + event_loop = QtCore.QEventLoop(None) + QtCore.QTimer.singleShot(100, event_loop.quit) + event_loop.exec_() + QtCore.QTimer.singleShot(100, event_loop.quit) + event_loop.exec() + + +def test_qthread_exec(): + """Test `QThread.exec_` and `QThread.exec_`""" + assert QtCore.QThread.exec_ is not None + assert QtCore.QThread.exec is not None + + +@pytest.mark.skipif( + PYSIDE2 and _parse_version(PYSIDE_VERSION) < _parse_version("5.15"), + reason="QEnum macro doesn't seem to be present on PySide2 <5.15", +) +def test_qenum(): + """Test QEnum macro""" + + class EnumTest(QtCore.QObject): + class Position(enum.IntEnum): + West = 0 + North = 1 + South = 2 + East = 3 + + QtCore.QEnum(Position) + + obj = EnumTest() + assert obj.metaObject().enumerator(0).name() == "Position" + + +def test_QLibraryInfo_location_and_path(): + """Test `QLibraryInfo.location` and `QLibraryInfo.path`""" + assert QtCore.QLibraryInfo.location is not None + assert ( + QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.PrefixPath) + is not None + ) + assert QtCore.QLibraryInfo.path is not None + assert QtCore.QLibraryInfo.path(QtCore.QLibraryInfo.PrefixPath) is not None + + +def test_QLibraryInfo_LibraryLocation_and_LibraryPath(): + """Test `QLibraryInfo.LibraryLocation` and `QLibraryInfo.LibraryPath`""" + assert QtCore.QLibraryInfo.LibraryLocation is not None + assert QtCore.QLibraryInfo.LibraryPath is not None + + +def test_QCoreApplication_exec_(qapp): + """Test `QtCore.QCoreApplication.exec_`""" + assert QtCore.QCoreApplication.exec_ is not None + app = QtCore.QCoreApplication.instance() or QtCore.QCoreApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtCore.QCoreApplication.instance().quit) + QtCore.QCoreApplication.exec_() + app = QtCore.QCoreApplication.instance() or QtCore.QCoreApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtCore.QCoreApplication.instance().quit) + app.exec_() + + +def test_QCoreApplication_exec(qapp): + """Test `QtCore.QCoreApplication.exec`""" + assert QtCore.QCoreApplication.exec is not None + app = QtCore.QCoreApplication.instance() or QtCore.QCoreApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtCore.QCoreApplication.instance().quit) + QtCore.QCoreApplication.exec() + app = QtCore.QCoreApplication.instance() or QtCore.QCoreApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtCore.QCoreApplication.instance().quit) + app.exec() + + +@pytest.mark.skipif( + PYQT5 or PYQT6, + reason="Doesn't seem to be present on PyQt5 and PyQt6", +) +def test_qtextstreammanipulator_exec(): + """Test `QTextStreamManipulator.exec_` and `QTextStreamManipulator.exec`""" + assert QtCore.QTextStreamManipulator.exec_ is not None + assert QtCore.QTextStreamManipulator.exec is not None + + +@pytest.mark.skipif( + PYSIDE2 or PYQT6, + reason="Doesn't seem to be present on PySide2 and PyQt6", +) +def test_QtCore_SignalInstance(): + class ClassWithSignal(QtCore.QObject): + signal = QtCore.Signal() + + instance = ClassWithSignal() + + assert isinstance(instance.signal, QtCore.SignalInstance) + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access", +) +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtCore.*.""" + assert ( + QtCore.QAbstractAnimation.Stopped + == QtCore.QAbstractAnimation.State.Stopped + ) + assert QtCore.QEvent.ActionAdded == QtCore.QEvent.Type.ActionAdded + assert QtCore.Qt.AlignLeft == QtCore.Qt.AlignmentFlag.AlignLeft + assert QtCore.Qt.Key_Return == QtCore.Qt.Key.Key_Return + assert QtCore.Qt.transparent == QtCore.Qt.GlobalColor.transparent + assert QtCore.Qt.Widget == QtCore.Qt.WindowType.Widget + assert QtCore.Qt.BackButton == QtCore.Qt.MouseButton.BackButton + assert QtCore.Qt.XButton1 == QtCore.Qt.MouseButton.XButton1 + assert ( + QtCore.Qt.BackgroundColorRole + == QtCore.Qt.ItemDataRole.BackgroundColorRole + ) + assert QtCore.Qt.TextColorRole == QtCore.Qt.ItemDataRole.TextColorRole + assert QtCore.Qt.MidButton == QtCore.Qt.MouseButton.MiddleButton + assert ( + QtCore.Qt.MouseButton.MidButton == QtCore.Qt.MouseButton.MiddleButton + ) + + +@pytest.mark.skipif( + PYSIDE2 and PYSIDE_VERSION.startswith("5.12.0"), + reason="Utility functions unavailable for PySide2 5.12.0", +) +def test_qtgui_namespace_mightBeRichText(): + """ + Test included elements (mightBeRichText) from module QtGui. + + See: https://doc.qt.io/qt-5/qt-sub-qtgui.html + """ + assert QtCore.Qt.mightBeRichText is not None + + +def test_itemflags_typedef(): + """ + Test existence of `QFlags` typedef `ItemFlags` that was removed from PyQt6 + """ + assert QtCore.Qt.ItemFlags is not None + assert QtCore.Qt.ItemFlags() == QtCore.Qt.ItemFlag(0) diff --git a/tests/test_qtdatavisualization.py b/tests/test_qtdatavisualization.py new file mode 100644 index 00000000..21b2cc85 --- /dev/null +++ b/tests/test_qtdatavisualization.py @@ -0,0 +1,88 @@ +import pytest + +from tests.utils import pytest_importorskip + + +def test_qtdatavisualization(): + """Test the qtpy.QtDataVisualization namespace""" + # Using import skip here since with Python 3 you need to install another package + # besides the base `PyQt5` or `PySide2`. + # For example in the case of `PyQt5` you need `PyQtDataVisualization` + + # QtDataVisualization + QtDataVisualization = pytest_importorskip("qtpy.QtDataVisualization") + assert QtDataVisualization.QScatter3DSeries is not None + assert QtDataVisualization.QSurfaceDataItem is not None + assert QtDataVisualization.QSurface3DSeries is not None + assert QtDataVisualization.QAbstract3DInputHandler is not None + assert QtDataVisualization.QHeightMapSurfaceDataProxy is not None + assert QtDataVisualization.QAbstractDataProxy is not None + assert QtDataVisualization.Q3DCamera is not None + assert QtDataVisualization.QAbstract3DGraph is not None + assert QtDataVisualization.QCustom3DVolume is not None + assert QtDataVisualization.Q3DInputHandler is not None + assert QtDataVisualization.QBarDataProxy is not None + assert QtDataVisualization.QSurfaceDataProxy is not None + assert QtDataVisualization.QScatterDataItem is not None + assert QtDataVisualization.Q3DLight is not None + assert QtDataVisualization.QScatterDataProxy is not None + assert QtDataVisualization.QValue3DAxis is not None + assert QtDataVisualization.Q3DBars is not None + assert QtDataVisualization.QBarDataItem is not None + assert QtDataVisualization.QItemModelBarDataProxy is not None + assert QtDataVisualization.Q3DTheme is not None + assert QtDataVisualization.QCustom3DItem is not None + assert QtDataVisualization.QItemModelScatterDataProxy is not None + assert QtDataVisualization.QValue3DAxisFormatter is not None + assert QtDataVisualization.QItemModelSurfaceDataProxy is not None + assert QtDataVisualization.Q3DScatter is not None + assert QtDataVisualization.QTouch3DInputHandler is not None + assert QtDataVisualization.QBar3DSeries is not None + assert QtDataVisualization.QAbstract3DAxis is not None + assert QtDataVisualization.Q3DScene is not None + assert QtDataVisualization.QCategory3DAxis is not None + assert QtDataVisualization.QAbstract3DSeries is not None + assert QtDataVisualization.Q3DObject is not None + assert QtDataVisualization.QCustom3DLabel is not None + assert QtDataVisualization.Q3DSurface is not None + assert QtDataVisualization.QLogValue3DAxisFormatter is not None + + # QtDatavisualization + # import qtpy to get alias for `QtDataVisualization` with lower `v` + qtpy = pytest_importorskip("qtpy") + + assert qtpy.QtDatavisualization.QScatter3DSeries is not None + assert qtpy.QtDatavisualization.QSurfaceDataItem is not None + assert qtpy.QtDatavisualization.QSurface3DSeries is not None + assert qtpy.QtDatavisualization.QAbstract3DInputHandler is not None + assert qtpy.QtDatavisualization.QHeightMapSurfaceDataProxy is not None + assert qtpy.QtDatavisualization.QAbstractDataProxy is not None + assert qtpy.QtDatavisualization.Q3DCamera is not None + assert qtpy.QtDatavisualization.QAbstract3DGraph is not None + assert qtpy.QtDatavisualization.QCustom3DVolume is not None + assert qtpy.QtDatavisualization.Q3DInputHandler is not None + assert qtpy.QtDatavisualization.QBarDataProxy is not None + assert qtpy.QtDatavisualization.QSurfaceDataProxy is not None + assert qtpy.QtDatavisualization.QScatterDataItem is not None + assert qtpy.QtDatavisualization.Q3DLight is not None + assert qtpy.QtDatavisualization.QScatterDataProxy is not None + assert qtpy.QtDatavisualization.QValue3DAxis is not None + assert qtpy.QtDatavisualization.Q3DBars is not None + assert qtpy.QtDatavisualization.QBarDataItem is not None + assert qtpy.QtDatavisualization.QItemModelBarDataProxy is not None + assert qtpy.QtDatavisualization.Q3DTheme is not None + assert qtpy.QtDatavisualization.QCustom3DItem is not None + assert qtpy.QtDatavisualization.QItemModelScatterDataProxy is not None + assert qtpy.QtDatavisualization.QValue3DAxisFormatter is not None + assert qtpy.QtDatavisualization.QItemModelSurfaceDataProxy is not None + assert qtpy.QtDatavisualization.Q3DScatter is not None + assert qtpy.QtDatavisualization.QTouch3DInputHandler is not None + assert qtpy.QtDatavisualization.QBar3DSeries is not None + assert qtpy.QtDatavisualization.QAbstract3DAxis is not None + assert qtpy.QtDatavisualization.Q3DScene is not None + assert qtpy.QtDatavisualization.QCategory3DAxis is not None + assert qtpy.QtDatavisualization.QAbstract3DSeries is not None + assert qtpy.QtDatavisualization.Q3DObject is not None + assert qtpy.QtDatavisualization.QCustom3DLabel is not None + assert qtpy.QtDatavisualization.Q3DSurface is not None + assert qtpy.QtDatavisualization.QLogValue3DAxisFormatter is not None diff --git a/tests/test_qtdbus.py b/tests/test_qtdbus.py new file mode 100644 index 00000000..41d78d7c --- /dev/null +++ b/tests/test_qtdbus.py @@ -0,0 +1,31 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION +from tests.utils import pytest_importorskip + + +def test_qtdbus(): + """Test the qtpy.QtDBus namespace""" + QtDBus = pytest_importorskip("qtpy.QtDBus") + + assert QtDBus.QDBusAbstractAdaptor is not None + assert QtDBus.QDBusAbstractInterface is not None + assert QtDBus.QDBusArgument is not None + assert QtDBus.QDBusConnection is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + QtDBus = pytest_importorskip("qtpy.QtDBus") + + assert ( + QtDBus.QDBusError.InvalidSignature + == QtDBus.QDBusError.ErrorType.InvalidSignature + ) diff --git a/tests/test_qtdesigner.py b/tests/test_qtdesigner.py new file mode 100644 index 00000000..5a312f5c --- /dev/null +++ b/tests/test_qtdesigner.py @@ -0,0 +1,47 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, PYSIDE2 +from tests.utils import pytest_importorskip + + +@pytest.mark.skipif(PYSIDE2, reason="QtDesigner is not available in PySide2") +def test_qtdesigner(): + """Test the qtpy.QtDesigner namespace.""" + QtDesigner = pytest_importorskip("qtpy.QtDesigner") + + assert QtDesigner.QAbstractExtensionFactory is not None + assert QtDesigner.QAbstractExtensionManager is not None + assert QtDesigner.QDesignerActionEditorInterface is not None + assert QtDesigner.QDesignerContainerExtension is not None + assert QtDesigner.QDesignerCustomWidgetCollectionInterface is not None + assert QtDesigner.QDesignerCustomWidgetInterface is not None + assert QtDesigner.QDesignerFormEditorInterface is not None + assert QtDesigner.QDesignerFormWindowCursorInterface is not None + assert QtDesigner.QDesignerFormWindowInterface is not None + assert QtDesigner.QDesignerFormWindowManagerInterface is not None + assert QtDesigner.QDesignerMemberSheetExtension is not None + assert QtDesigner.QDesignerObjectInspectorInterface is not None + assert QtDesigner.QDesignerPropertyEditorInterface is not None + assert QtDesigner.QDesignerPropertySheetExtension is not None + assert QtDesigner.QDesignerTaskMenuExtension is not None + assert QtDesigner.QDesignerWidgetBoxInterface is not None + assert QtDesigner.QExtensionFactory is not None + assert QtDesigner.QExtensionManager is not None + assert QtDesigner.QFormBuilder is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + QtDesigner = pytest_importorskip("qtpy.QtDesigner") + + assert ( + QtDesigner.QDesignerFormWindowInterface.EditFeature + == QtDesigner.QDesignerFormWindowInterface.FeatureFlag.EditFeature + ) diff --git a/tests/test_qtgui.py b/tests/test_qtgui.py new file mode 100644 index 00000000..ed5dd4b9 --- /dev/null +++ b/tests/test_qtgui.py @@ -0,0 +1,248 @@ +"""Test QtGui.""" + +import sys + +import pytest + +from qtpy import ( + PYQT5, + PYQT_VERSION, + PYSIDE2, + PYSIDE6, + QT_VERSION, + QtCore, + QtGui, + QtWidgets, + _parse_version, +) + + +def test_qfontmetrics_width(qtbot): + """Test QFontMetrics and QFontMetricsF width""" + assert QtGui.QFontMetrics.width is not None + assert QtGui.QFontMetricsF.width is not None + font = QtGui.QFont("times", 24) + font_metrics = QtGui.QFontMetrics(font) + font_metricsF = QtGui.QFontMetricsF(font) + width = font_metrics.width("Test") + widthF = font_metricsF.width("Test") + assert width in range(39, 62) + assert 38 <= widthF <= 63 + + +def test_qdrag_functions(qtbot): + """Test functions mapping for QtGui.QDrag.""" + assert QtGui.QDrag.exec_ is not None + drag = QtGui.QDrag(None) + drag.exec_() + + +def test_QGuiApplication_exec_(): + """Test `QtGui.QGuiApplication.exec_`""" + assert QtGui.QGuiApplication.exec_ is not None + app = QtGui.QGuiApplication.instance() or QtGui.QGuiApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtGui.QGuiApplication.instance().quit) + QtGui.QGuiApplication.exec_() + app = QtGui.QGuiApplication.instance() or QtGui.QGuiApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtGui.QGuiApplication.instance().quit) + app.exec_() + + +def test_what_moved_to_qtgui_in_qt6(): + """Test what has been moved to QtGui in Qt6""" + assert QtGui.QAction is not None + assert QtGui.QActionGroup is not None + assert QtGui.QFileSystemModel is not None + assert QtGui.QShortcut is not None + assert QtGui.QUndoCommand is not None + assert QtGui.QUndoStack is not None + + +def test_qtextdocument_functions(pdf_writer): + """Test functions mapping for QtGui.QTextDocument.""" + assert QtGui.QTextDocument.print_ is not None + text_document = QtGui.QTextDocument("Test") + print_device, output_path = pdf_writer + text_document.print_(print_device) + assert output_path.exists() + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access", +) +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtWidgets.*.""" + assert QtGui.QColor.Rgb == QtGui.QColor.Spec.Rgb + assert QtGui.QFont.AllUppercase == QtGui.QFont.Capitalization.AllUppercase + assert QtGui.QIcon.Normal == QtGui.QIcon.Mode.Normal + assert QtGui.QImage.Format_Invalid == QtGui.QImage.Format.Format_Invalid + + +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +def test_QSomethingEvent_pos_functions(qtbot): + """ + Test `QMouseEvent.pos` and related functions removed in Qt 6, + and `QMouseEvent.position`, etc., missing from Qt 5. + """ + + class Window(QtWidgets.QMainWindow): + def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None: + assert event.globalPos() - event.pos() == self.mapToParent( + QtCore.QPoint(0, 0), + ) + assert event.pos().x() == event.x() + assert event.pos().y() == event.y() + assert event.globalPos().x() == event.globalX() + assert event.globalPos().y() == event.globalY() + assert event.position().x() == event.pos().x() + assert event.position().y() == event.pos().y() + assert event.globalPosition().x() == event.globalPos().x() + assert event.globalPosition().y() == event.globalPos().y() + + event.accept() + + window = Window() + window.setMinimumSize(320, 240) # ensure the window is of sufficient size + window.show() + + with qtbot.waitExposed(window): + qtbot.mouseMove(window, QtCore.QPoint(42, 6 * 9)) + qtbot.mouseDClick(window, QtCore.Qt.LeftButton) + + # the rest of the functions are not actually tested + # QSinglePointEvent (Qt6) child classes checks + for _class in ("QNativeGestureEvent", "QEnterEvent", "QTabletEvent"): + for _function in ( + "pos", + "x", + "y", + "globalPos", + "globalX", + "globalY", + "position", + "globalPosition", + ): + assert hasattr(getattr(QtGui, _class), _function) + + # QHoverEvent checks + for _function in ("pos", "x", "y", "position"): + assert hasattr(QtGui.QHoverEvent, _function) + + # QDropEvent and child classes checks + for _class in ("QDropEvent", "QDragMoveEvent", "QDragEnterEvent"): + for _function in ("pos", "posF", "position"): + assert hasattr(getattr(QtGui, _class), _function) + + +@pytest.mark.skipif( + not (PYSIDE2 or PYSIDE6), + reason="PySide{2,6} specific test", +) +def test_qtextcursor_moveposition(): + """Test monkeypatched QTextCursor.movePosition""" + doc = QtGui.QTextDocument("foo bar baz") + cursor = QtGui.QTextCursor(doc) + + assert not cursor.movePosition(QtGui.QTextCursor.Start) + assert cursor.movePosition( + QtGui.QTextCursor.EndOfWord, + mode=QtGui.QTextCursor.KeepAnchor, + ) + assert cursor.selectedText() == "foo" + + assert cursor.movePosition(QtGui.QTextCursor.Start) + assert cursor.movePosition( + QtGui.QTextCursor.WordRight, + n=2, + mode=QtGui.QTextCursor.KeepAnchor, + ) + assert cursor.selectedText() == "foo bar " + + assert cursor.movePosition(QtGui.QTextCursor.Start) + assert cursor.position() == cursor.anchor() + assert cursor.movePosition( + QtGui.QTextCursor.NextWord, + QtGui.QTextCursor.KeepAnchor, + 3, + ) + assert cursor.selectedText() == "foo bar baz" + + +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +def test_QAction_functions(qtbot): + """Test `QtGui.QAction.setShortcut` compatibility with Qt6 types.""" + action = QtGui.QAction("QtPy", None) + action.setShortcut(QtGui.QKeySequence.UnknownKey) + action.setShortcuts([QtGui.QKeySequence.UnknownKey]) + action.setShortcuts(QtGui.QKeySequence.UnknownKey) + action.setShortcut(QtCore.Qt.Key_F1) + action.setShortcuts([QtCore.Qt.Key_F1]) + # The following line fails even for Qt6 == 6.6. + # Don't test the function with a single `QtCore.Qt.Key` argument. + # See the following test. + # action.setShortcuts(QtCore.Qt.Key_F1) + + +@pytest.mark.skipif( + _parse_version(QT_VERSION) < _parse_version("6.5.0"), + reason="Qt6 >= 6.5 specific test", +) +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +@pytest.mark.xfail(strict=True) +def test_QAction_functions_fail(qtbot): + """Test `QtGui.QAction.setShortcuts` compatibility with `QtCore.Qt.Key` type.""" + action = QtGui.QAction("QtPy", None) + # The following line is wrong even for Qt6 == 6.6. + action.setShortcuts(QtCore.Qt.Key_F1) + + +def test_opengl_imports(): + """ + Test for presence of QOpenGL* classes. + + These classes were members of QtGui in Qt5, but moved to QtOpenGL in Qt6. + QtPy makes them available in QtGui to maintain compatibility. + """ + + assert QtGui.QOpenGLBuffer is not None + assert QtGui.QOpenGLContext is not None + assert QtGui.QOpenGLContextGroup is not None + assert QtGui.QOpenGLDebugLogger is not None + assert QtGui.QOpenGLDebugMessage is not None + assert QtGui.QOpenGLFramebufferObject is not None + assert QtGui.QOpenGLFramebufferObjectFormat is not None + assert QtGui.QOpenGLPixelTransferOptions is not None + assert QtGui.QOpenGLShader is not None + assert QtGui.QOpenGLShaderProgram is not None + assert QtGui.QOpenGLTexture is not None + assert QtGui.QOpenGLTextureBlitter is not None + assert QtGui.QOpenGLVersionProfile is not None + assert QtGui.QOpenGLVertexArrayObject is not None + assert QtGui.QOpenGLWindow is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access", +) +def test_theme_icon_enum(): + """Test `QIcon.ThemeIcon` presence and work.""" + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.AddressBookNew) diff --git a/tests/test_qthelp.py b/tests/test_qthelp.py new file mode 100644 index 00000000..e2f6915d --- /dev/null +++ b/tests/test_qthelp.py @@ -0,0 +1,35 @@ +"""Test for QtHelp namespace.""" + +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QtHelp + + +def test_qthelp(): + """Test the qtpy.QtHelp namespace.""" + assert QtHelp.QHelpContentItem is not None + assert QtHelp.QHelpContentModel is not None + assert QtHelp.QHelpContentWidget is not None + assert QtHelp.QHelpEngine is not None + assert QtHelp.QHelpEngineCore is not None + assert QtHelp.QHelpIndexModel is not None + assert QtHelp.QHelpIndexWidget is not None + assert QtHelp.QHelpSearchEngine is not None + assert QtHelp.QHelpSearchQuery is not None + assert QtHelp.QHelpSearchQueryWidget is not None + assert QtHelp.QHelpSearchResultWidget is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtHelp.QHelpSearchQuery.DEFAULT + == QtHelp.QHelpSearchQuery.FieldName.DEFAULT + ) diff --git a/tests/test_qtlocation.py b/tests/test_qtlocation.py new file mode 100644 index 00000000..f23a3887 --- /dev/null +++ b/tests/test_qtlocation.py @@ -0,0 +1,54 @@ +import pytest + +from qtpy import PYQT5, PYSIDE2 + + +@pytest.mark.skipif( + not (PYQT5 or PYSIDE2), + reason="Only available in Qt5 bindings", +) +def test_qtlocation(): + """Test the qtpy.QtLocation namespace""" + from qtpy import QtLocation + + if PYSIDE2: + assert QtLocation.QGeoServiceProviderFactory is not None + + assert QtLocation.QGeoCodeReply is not None + assert QtLocation.QGeoCodingManager is not None + assert QtLocation.QGeoCodingManagerEngine is not None + assert QtLocation.QGeoManeuver is not None + assert QtLocation.QGeoRoute is not None + assert QtLocation.QGeoRouteReply is not None + assert QtLocation.QGeoRouteRequest is not None + assert QtLocation.QGeoRouteSegment is not None + assert QtLocation.QGeoRoutingManager is not None + assert QtLocation.QGeoRoutingManagerEngine is not None + assert QtLocation.QGeoServiceProvider is not None + assert QtLocation.QPlace is not None + assert QtLocation.QPlaceAttribute is not None + assert QtLocation.QPlaceCategory is not None + assert QtLocation.QPlaceContactDetail is not None + assert QtLocation.QPlaceContent is not None + assert QtLocation.QPlaceContentReply is not None + assert QtLocation.QPlaceContentRequest is not None + assert QtLocation.QPlaceDetailsReply is not None + assert QtLocation.QPlaceEditorial is not None + assert QtLocation.QPlaceIcon is not None + assert QtLocation.QPlaceIdReply is not None + assert QtLocation.QPlaceImage is not None + assert QtLocation.QPlaceManager is not None + assert QtLocation.QPlaceManagerEngine is not None + assert QtLocation.QPlaceMatchReply is not None + assert QtLocation.QPlaceMatchRequest is not None + assert QtLocation.QPlaceProposedSearchResult is not None + assert QtLocation.QPlaceRatings is not None + assert QtLocation.QPlaceReply is not None + assert QtLocation.QPlaceResult is not None + assert QtLocation.QPlaceReview is not None + assert QtLocation.QPlaceSearchReply is not None + assert QtLocation.QPlaceSearchRequest is not None + assert QtLocation.QPlaceSearchResult is not None + assert QtLocation.QPlaceSearchSuggestionReply is not None + assert QtLocation.QPlaceSupplier is not None + assert QtLocation.QPlaceUser is not None diff --git a/tests/test_qtmacextras.py b/tests/test_qtmacextras.py new file mode 100644 index 00000000..b8aaee71 --- /dev/null +++ b/tests/test_qtmacextras.py @@ -0,0 +1,23 @@ +import sys + +import pytest + +from qtpy import PYQT6, PYSIDE6 +from tests.utils import pytest_importorskip, using_conda + + +@pytest.mark.skipif( + PYQT6 or PYSIDE6, + reason="Not available on Qt6-based bindings", +) +@pytest.mark.skipif( + sys.platform != "darwin" or using_conda(), + reason="Only available in Qt5 bindings > 5.9 with pip on mac in CIs", +) +def test_qtmacextras(): + """Test the qtpy.QtMacExtras namespace""" + QtMacExtras = pytest_importorskip("qtpy.QtMacExtras") + + assert QtMacExtras.QMacPasteboardMime is not None + assert QtMacExtras.QMacToolBar is not None + assert QtMacExtras.QMacToolBarItem is not None diff --git a/tests/test_qtmultimedia.py b/tests/test_qtmultimedia.py new file mode 100644 index 00000000..8e493e13 --- /dev/null +++ b/tests/test_qtmultimedia.py @@ -0,0 +1,29 @@ +import pytest + +from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE6, QtMultimedia + + +def test_qtmultimedia(): + """Test the qtpy.QtMultimedia namespace""" + assert QtMultimedia.QAudio is not None + assert QtMultimedia.QAudioInput is not None + + if not (PYSIDE6 or PYQT6): + assert QtMultimedia.QAbstractVideoBuffer is not None + assert QtMultimedia.QAudioDeviceInfo is not None + assert QtMultimedia.QSound is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtMultimedia.QAudio.LinearVolumeScale + == QtMultimedia.QAudio.VolumeScale.LinearVolumeScale + ) diff --git a/tests/test_qtmultimediawidgets.py b/tests/test_qtmultimediawidgets.py new file mode 100644 index 00000000..6f80e4d6 --- /dev/null +++ b/tests/test_qtmultimediawidgets.py @@ -0,0 +1,15 @@ +"""Test QtMultimediaWidgets.""" + + +from qtpy import PYQT5, PYSIDE2 + + +def test_qtmultimediawidgets(): + """Test the qtpy.QtMultimediaWidgets namespace""" + from qtpy import QtMultimediaWidgets + + if PYQT5 or PYSIDE2: + assert QtMultimediaWidgets.QCameraViewfinder is not None + # assert QtMultimediaWidgets.QVideoWidgetControl is not None + assert QtMultimediaWidgets.QGraphicsVideoItem is not None + assert QtMultimediaWidgets.QVideoWidget is not None diff --git a/tests/test_qtnetwork.py b/tests/test_qtnetwork.py new file mode 100644 index 00000000..b4f6980d --- /dev/null +++ b/tests/test_qtnetwork.py @@ -0,0 +1,57 @@ +import pytest + +from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE2, PYSIDE6, QtNetwork + + +def test_qtnetwork(): + """Test the qtpy.QtNetwork namespace""" + assert QtNetwork.QAbstractNetworkCache is not None + assert QtNetwork.QNetworkCacheMetaData is not None + if not PYSIDE2: + assert QtNetwork.QHttpMultiPart is not None + assert QtNetwork.QHttpPart is not None + assert QtNetwork.QNetworkAccessManager is not None + assert QtNetwork.QNetworkCookie is not None + assert QtNetwork.QNetworkCookieJar is not None + assert QtNetwork.QNetworkDiskCache is not None + assert QtNetwork.QNetworkReply is not None + assert QtNetwork.QNetworkRequest is not None + if not (PYSIDE6 or PYQT6): + assert QtNetwork.QNetworkConfigurationManager is not None + assert QtNetwork.QNetworkConfiguration is not None + assert QtNetwork.QNetworkSession is not None + assert QtNetwork.QAuthenticator is not None + assert QtNetwork.QHostAddress is not None + assert QtNetwork.QHostInfo is not None + assert QtNetwork.QNetworkAddressEntry is not None + assert QtNetwork.QNetworkInterface is not None + assert QtNetwork.QNetworkProxy is not None + assert QtNetwork.QNetworkProxyFactory is not None + assert QtNetwork.QNetworkProxyQuery is not None + assert QtNetwork.QAbstractSocket is not None + assert QtNetwork.QLocalServer is not None + assert QtNetwork.QLocalSocket is not None + assert QtNetwork.QTcpServer is not None + assert QtNetwork.QTcpSocket is not None + assert QtNetwork.QUdpSocket is not None + assert QtNetwork.QSslCertificate is not None + assert QtNetwork.QSslCipher is not None + assert QtNetwork.QSslConfiguration is not None + assert QtNetwork.QSslError is not None + assert QtNetwork.QSslKey is not None + assert QtNetwork.QSslSocket is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtNetwork.QAbstractSocket.ShareAddress + == QtNetwork.QAbstractSocket.BindFlag.ShareAddress + ) diff --git a/tests/test_qtnetworkauth.py b/tests/test_qtnetworkauth.py new file mode 100644 index 00000000..eccc6da7 --- /dev/null +++ b/tests/test_qtnetworkauth.py @@ -0,0 +1,17 @@ +import pytest + +from qtpy import PYSIDE2 +from tests.utils import pytest_importorskip + + +@pytest.mark.skipif(PYSIDE2, reason="Not available for PySide2") +def test_qtnetworkauth(): + """Test the qtpy.QtNetworkAuth namespace""" + QtNetworkAuth = pytest_importorskip("qtpy.QtNetworkAuth") + + assert QtNetworkAuth.QAbstractOAuth is not None + assert QtNetworkAuth.QAbstractOAuth2 is not None + assert QtNetworkAuth.QAbstractOAuthReplyHandler is not None + assert QtNetworkAuth.QOAuth1 is not None + assert QtNetworkAuth.QOAuth1Signature is not None + assert QtNetworkAuth.QOAuth2AuthorizationCodeFlow is not None diff --git a/tests/test_qtopengl.py b/tests/test_qtopengl.py new file mode 100644 index 00000000..2ff05ae5 --- /dev/null +++ b/tests/test_qtopengl.py @@ -0,0 +1,39 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QtOpenGL + + +def test_qtopengl(): + """Test the qtpy.QtOpenGL namespace""" + assert QtOpenGL.QOpenGLBuffer is not None + assert QtOpenGL.QOpenGLContext is not None + assert QtOpenGL.QOpenGLContextGroup is not None + assert QtOpenGL.QOpenGLDebugLogger is not None + assert QtOpenGL.QOpenGLDebugMessage is not None + assert QtOpenGL.QOpenGLFramebufferObject is not None + assert QtOpenGL.QOpenGLFramebufferObjectFormat is not None + assert QtOpenGL.QOpenGLPixelTransferOptions is not None + assert QtOpenGL.QOpenGLShader is not None + assert QtOpenGL.QOpenGLShaderProgram is not None + assert QtOpenGL.QOpenGLTexture is not None + assert QtOpenGL.QOpenGLTextureBlitter is not None + assert QtOpenGL.QOpenGLVersionProfile is not None + assert QtOpenGL.QOpenGLVertexArrayObject is not None + assert QtOpenGL.QOpenGLWindow is not None + # We do not test for QOpenGLTimeMonitor or QOpenGLTimerQuery as + # they are not present on some architectures such as armhf + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtOpenGL.QOpenGLBuffer.RangeRead + == QtOpenGL.QOpenGLBuffer.RangeAccessFlag.RangeRead + ) diff --git a/tests/test_qtopenglwidgets.py b/tests/test_qtopenglwidgets.py new file mode 100644 index 00000000..953c9d5c --- /dev/null +++ b/tests/test_qtopenglwidgets.py @@ -0,0 +1,22 @@ +import pytest + +from qtpy import PYQT5, PYSIDE2 + + +@pytest.mark.skipif(PYSIDE2 or PYQT5, reason="Not available in PySide2/PyQt5") +def test_qtopenglwidgets(): + """Test the qtpy.QtOpenGLWidgets namespace""" + from qtpy import QtOpenGLWidgets + + assert QtOpenGLWidgets.QOpenGLWidget is not None + + +@pytest.mark.skipif(PYSIDE2 or PYQT5, reason="Not available in PySide2/PyQt5") +def test_enum_access(): + """Test scoped and unscoped enum access.""" + from qtpy import QtOpenGLWidgets + + assert ( + QtOpenGLWidgets.QOpenGLWidget.NoPartialUpdate + == QtOpenGLWidgets.QOpenGLWidget.UpdateBehavior.NoPartialUpdate + ) diff --git a/tests/test_qtpdf.py b/tests/test_qtpdf.py new file mode 100644 index 00000000..14dcf767 --- /dev/null +++ b/tests/test_qtpdf.py @@ -0,0 +1,10 @@ +from tests.utils import pytest_importorskip + + +def test_qtpdf(): + """Test the qtpy.QtPdf namespace""" + QtPdf = pytest_importorskip("qtpy.QtPdf") + + assert QtPdf.QPdfDocument is not None + assert QtPdf.QPdfLink is not None + assert QtPdf.QPdfSelection is not None diff --git a/tests/test_qtpdfwidgets.py b/tests/test_qtpdfwidgets.py new file mode 100644 index 00000000..270afe95 --- /dev/null +++ b/tests/test_qtpdfwidgets.py @@ -0,0 +1,8 @@ +from tests.utils import pytest_importorskip + + +def test_qtpdfwidgets(): + """Test the qtpy.QtPdfWidgets namespace""" + QtPdfWidgets = pytest_importorskip("qtpy.QtPdfWidgets") + + assert QtPdfWidgets.QPdfView is not None diff --git a/tests/test_qtpositioning.py b/tests/test_qtpositioning.py new file mode 100644 index 00000000..3ade91d1 --- /dev/null +++ b/tests/test_qtpositioning.py @@ -0,0 +1,54 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QT6 +from tests.utils import using_conda + + +@pytest.mark.skipif( + QT6 and using_conda(), + reason="QPositioning bindings not included in Conda qt-main >= 6.4.3.", +) +def test_qtpositioning(): + """Test the qtpy.QtPositioning namespace""" + from qtpy import QtPositioning + + assert QtPositioning.QGeoAddress is not None + assert QtPositioning.QGeoAreaMonitorInfo is not None + assert QtPositioning.QGeoAreaMonitorSource is not None + assert QtPositioning.QGeoCircle is not None + assert QtPositioning.QGeoCoordinate is not None + assert QtPositioning.QGeoLocation is not None + assert QtPositioning.QGeoPath is not None + # CI for 3.7 uses Qt 5.9 + # assert QtPositioning.QGeoPolygon is not None # New in Qt 5.10 + assert QtPositioning.QGeoPositionInfo is not None + assert QtPositioning.QGeoPositionInfoSource is not None + # QGeoPositionInfoSourceFactory is not available in PyQt + # assert QtPositioning.QGeoPositionInfoSourceFactory is not None # New in Qt 5.2 + # assert QtPositioning.QGeoPositionInfoSourceFactoryV2 is not None # New in Qt 5.14 + assert QtPositioning.QGeoRectangle is not None + assert QtPositioning.QGeoSatelliteInfo is not None + assert QtPositioning.QGeoSatelliteInfoSource is not None + assert QtPositioning.QGeoShape is not None + assert QtPositioning.QNmeaPositionInfoSource is not None + + +@pytest.mark.skipif( + QT6 and using_conda(), + reason="QPositioning bindings not included in Conda qt-main >= 6.4.3.", +) +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + from qtpy import QtPositioning + + assert ( + QtPositioning.QGeoShape.PolygonType + == QtPositioning.QGeoShape.ShapeType.PolygonType + ) diff --git a/tests/test_qtprintsupport.py b/tests/test_qtprintsupport.py new file mode 100644 index 00000000..ab7655ff --- /dev/null +++ b/tests/test_qtprintsupport.py @@ -0,0 +1,49 @@ +"""Test QtPrintSupport.""" + +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QtPrintSupport + + +def test_qtprintsupport(): + """Test the qtpy.QtPrintSupport namespace""" + assert QtPrintSupport.QAbstractPrintDialog is not None + assert QtPrintSupport.QPageSetupDialog is not None + assert QtPrintSupport.QPrintDialog is not None + assert QtPrintSupport.QPrintPreviewDialog is not None + assert QtPrintSupport.QPrintEngine is not None + assert QtPrintSupport.QPrinter is not None + assert QtPrintSupport.QPrinterInfo is not None + assert QtPrintSupport.QPrintPreviewWidget is not None + + +def test_qpagesetupdialog_exec_(): + """Test qtpy.QtPrintSupport.QPageSetupDialog exec_""" + assert QtPrintSupport.QPageSetupDialog.exec_ is not None + + +def test_qprintdialog_exec_(): + """Test qtpy.QtPrintSupport.QPrintDialog exec_""" + assert QtPrintSupport.QPrintDialog.exec_ is not None + + +def test_qprintpreviewwidget_print_(qtbot): + """Test qtpy.QtPrintSupport.QPrintPreviewWidget print_""" + assert QtPrintSupport.QPrintPreviewWidget.print_ is not None + preview_widget = QtPrintSupport.QPrintPreviewWidget() + preview_widget.print_() + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access""" + assert ( + QtPrintSupport.QPrinter.HighResolution + == QtPrintSupport.QPrinter.PrinterMode.HighResolution + ) diff --git a/tests/test_qtpurchasing.py b/tests/test_qtpurchasing.py new file mode 100644 index 00000000..8003bede --- /dev/null +++ b/tests/test_qtpurchasing.py @@ -0,0 +1,10 @@ +from tests.utils import pytest_importorskip + + +def test_qtpurchasing(): + """Test the qtpy.QtPurchasing namespace""" + QtPurchasing = pytest_importorskip("qtpy.QtPurchasing") + + assert QtPurchasing.QInAppProduct is not None + assert QtPurchasing.QInAppStore is not None + assert QtPurchasing.QInAppTransaction is not None diff --git a/tests/test_qtqml.py b/tests/test_qtqml.py new file mode 100644 index 00000000..00a45b35 --- /dev/null +++ b/tests/test_qtqml.py @@ -0,0 +1,47 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, PYSIDE2, PYSIDE6, QtQml + + +def test_qtqml(): + """Test the qtpy.QtQml namespace""" + assert QtQml.QJSEngine is not None + assert QtQml.QJSValue is not None + assert QtQml.QJSValueIterator is not None + assert QtQml.QQmlAbstractUrlInterceptor is not None + assert QtQml.QQmlApplicationEngine is not None + assert QtQml.QQmlComponent is not None + assert QtQml.QQmlContext is not None + assert QtQml.QQmlEngine is not None + assert QtQml.QQmlImageProviderBase is not None + assert QtQml.QQmlError is not None + assert QtQml.QQmlExpression is not None + assert QtQml.QQmlExtensionPlugin is not None + assert QtQml.QQmlFileSelector is not None + assert QtQml.QQmlIncubationController is not None + assert QtQml.QQmlIncubator is not None + if not (PYSIDE2 or PYSIDE6): + # https://wiki.qt.io/Qt_for_Python_Missing_Bindings#QtQml + assert QtQml.QQmlListProperty is not None + assert QtQml.QQmlListReference is not None + assert QtQml.QQmlNetworkAccessManagerFactory is not None + assert QtQml.QQmlParserStatus is not None + assert QtQml.QQmlProperty is not None + assert QtQml.QQmlPropertyValueSource is not None + assert QtQml.QQmlScriptString is not None + assert QtQml.QQmlPropertyMap is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtQml.QQmlAbstractUrlInterceptor.QmlFile + == QtQml.QQmlAbstractUrlInterceptor.DataType.QmlFile + ) diff --git a/tests/test_qtquick.py b/tests/test_qtquick.py new file mode 100644 index 00000000..7eb584db --- /dev/null +++ b/tests/test_qtquick.py @@ -0,0 +1,63 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, PYSIDE2, QtQuick + + +def test_qtquick(): + """Test the qtpy.QtQuick namespace""" + if PYQT5: + assert QtQuick.QQuickCloseEvent is not None + assert QtQuick.QSGFlatColorMaterial is not None + assert QtQuick.QSGImageNode is not None + assert QtQuick.QSGMaterial is not None + assert QtQuick.QSGMaterialShader is not None + assert QtQuick.QSGOpaqueTextureMaterial is not None + assert QtQuick.QSGRectangleNode is not None + assert QtQuick.QSGRenderNode is not None + assert QtQuick.QSGRendererInterface is not None + assert QtQuick.QSGTextureMaterial is not None + assert QtQuick.QSGVertexColorMaterial is not None + + assert QtQuick.QQuickAsyncImageProvider is not None + assert QtQuick.QQuickFramebufferObject is not None + assert QtQuick.QQuickImageProvider is not None + assert QtQuick.QQuickImageResponse is not None + assert QtQuick.QQuickItem is not None + assert QtQuick.QQuickItemGrabResult is not None + assert QtQuick.QQuickPaintedItem is not None + assert QtQuick.QQuickRenderControl is not None + assert QtQuick.QQuickTextDocument is not None + assert QtQuick.QQuickTextureFactory is not None + assert QtQuick.QQuickView is not None + assert QtQuick.QQuickWindow is not None + if PYQT5 or PYSIDE2: + assert QtQuick.QSGAbstractRenderer is not None + assert QtQuick.QSGEngine is not None + assert QtQuick.QSGBasicGeometryNode is not None + assert QtQuick.QSGClipNode is not None + assert QtQuick.QSGDynamicTexture is not None + assert QtQuick.QSGGeometry is not None + assert QtQuick.QSGGeometryNode is not None + assert QtQuick.QSGMaterialType is not None + assert QtQuick.QSGNode is not None + assert QtQuick.QSGOpacityNode is not None + assert QtQuick.QSGSimpleRectNode is not None + assert QtQuick.QSGSimpleTextureNode is not None + assert QtQuick.QSGTexture is not None + assert QtQuick.QSGTextureProvider is not None + assert QtQuick.QSGTransformNode is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtQuick.QQuickItem.TopLeft + == QtQuick.QQuickItem.TransformOrigin.TopLeft + ) diff --git a/tests/test_qtquick3d.py b/tests/test_qtquick3d.py new file mode 100644 index 00000000..43d45ba8 --- /dev/null +++ b/tests/test_qtquick3d.py @@ -0,0 +1,10 @@ +from tests.utils import pytest_importorskip + + +def test_qtquick3d(): + """Test the qtpy.QtQuick3D namespace""" + QtQuick3D = pytest_importorskip("qtpy.QtQuick3D") + + assert QtQuick3D.QQuick3D is not None + assert QtQuick3D.QQuick3DGeometry is not None + assert QtQuick3D.QQuick3DObject is not None diff --git a/tests/test_qtquickcontrols2.py b/tests/test_qtquickcontrols2.py new file mode 100644 index 00000000..9d606c4a --- /dev/null +++ b/tests/test_qtquickcontrols2.py @@ -0,0 +1,8 @@ +from tests.utils import pytest_importorskip + + +def test_qtquickcontrols2(): + """Test the qtpy.QtQuickControls2 namespace""" + QtQuickControls2 = pytest_importorskip("qtpy.QtQuickControls2") + + assert QtQuickControls2.QQuickStyle is not None diff --git a/tests/test_qtquickwidgets.py b/tests/test_qtquickwidgets.py new file mode 100644 index 00000000..e2a0bde8 --- /dev/null +++ b/tests/test_qtquickwidgets.py @@ -0,0 +1,23 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, QtQuickWidgets + + +def test_qtquickwidgets(): + """Test the qtpy.QtQuickWidgets namespace""" + assert QtQuickWidgets.QQuickWidget is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtQuickWidgets.QQuickWidget.Null + == QtQuickWidgets.QQuickWidget.Status.Null + ) diff --git a/tests/test_qtremoteobjects.py b/tests/test_qtremoteobjects.py new file mode 100644 index 00000000..5778ce43 --- /dev/null +++ b/tests/test_qtremoteobjects.py @@ -0,0 +1,12 @@ +from tests.utils import pytest_importorskip + + +def test_qtremoteobjects(): + """Test the qtpy.QtRemoteObjects namespace""" + QtRemoteObjects = pytest_importorskip("qtpy.QtRemoteObjects") + + assert QtRemoteObjects.QRemoteObjectAbstractPersistedStore is not None + assert QtRemoteObjects.QRemoteObjectDynamicReplica is not None + assert QtRemoteObjects.QRemoteObjectHost is not None + assert QtRemoteObjects.QRemoteObjectHostBase is not None + assert QtRemoteObjects.QRemoteObjectNode is not None diff --git a/tests/test_qtscxml.py b/tests/test_qtscxml.py new file mode 100644 index 00000000..a2714740 --- /dev/null +++ b/tests/test_qtscxml.py @@ -0,0 +1,10 @@ +from tests.utils import pytest_importorskip + + +def test_qtscxml(): + """Test the qtpy.QtScxml namespace""" + QtScxml = pytest_importorskip("qtpy.QtScxml") + + assert QtScxml.QScxmlCompiler is not None + assert QtScxml.QScxmlDynamicScxmlServiceFactory is not None + assert QtScxml.QScxmlExecutableContent is not None diff --git a/tests/test_qtsensors.py b/tests/test_qtsensors.py new file mode 100644 index 00000000..c41f24a8 --- /dev/null +++ b/tests/test_qtsensors.py @@ -0,0 +1,29 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION + + +def test_qtsensors(): + """Test the qtpy.QtSensors namespace""" + QtSensors = pytest.importorskip("qtpy.QtSensors") + + assert QtSensors.QAccelerometer is not None + assert QtSensors.QAccelerometerFilter is not None + assert QtSensors.QAccelerometerReading is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + QtSensors = pytest.importorskip("qtpy.QtSensors") + + assert ( + QtSensors.QSensor.FixedOrientation + == QtSensors.QSensor.AxesOrientationMode.FixedOrientation + ) diff --git a/tests/test_qtserialport.py b/tests/test_qtserialport.py new file mode 100644 index 00000000..a50c0ea2 --- /dev/null +++ b/tests/test_qtserialport.py @@ -0,0 +1,12 @@ +import pytest + +from qtpy import PYSIDE2 + + +@pytest.mark.skipif(PYSIDE2, reason="Not available in CI") +def test_qtserialport(): + """Test the qtpy.QtSerialPort namespace""" + QtSerialPort = pytest.importorskip("qtpy.QtSerialPort") + + assert QtSerialPort.QSerialPort is not None + assert QtSerialPort.QSerialPortInfo is not None diff --git a/tests/test_qtsql.py b/tests/test_qtsql.py new file mode 100644 index 00000000..aa6ecd8d --- /dev/null +++ b/tests/test_qtsql.py @@ -0,0 +1,101 @@ +"""Test QtSql.""" + +import sys + +import pytest + +from qtpy import PYQT5, PYQT_VERSION, PYSIDE2, PYSIDE_VERSION, QtSql + + +@pytest.fixture +def database_connection(): + """Create a database connection""" + connection = QtSql.QSqlDatabase.addDatabase("QSQLITE") + yield connection + connection.close() + + +def test_qtsql(): + """Test the qtpy.QtSql namespace""" + assert QtSql.QSqlDatabase is not None + assert QtSql.QSqlDriverCreatorBase is not None + assert QtSql.QSqlDriver is not None + assert QtSql.QSqlError is not None + assert QtSql.QSqlField is not None + assert QtSql.QSqlIndex is not None + assert QtSql.QSqlQuery is not None + assert QtSql.QSqlRecord is not None + assert QtSql.QSqlResult is not None + assert QtSql.QSqlQueryModel is not None + assert QtSql.QSqlRelationalDelegate is not None + assert QtSql.QSqlRelation is not None + assert QtSql.QSqlRelationalTableModel is not None + assert QtSql.QSqlTableModel is not None + + # Following modules are not (yet) part of any wrapper: + # QSqlDriverCreator, QSqlDriverPlugin + + +@pytest.mark.skipif( + sys.platform == "win32" and PYSIDE2 and PYSIDE_VERSION.startswith("5.13"), + reason="SQLite driver unavailable on PySide 5.13.2 with Windows", +) +def test_qtsql_members_aliases(database_connection): + """ + Test aliased methods over qtpy.QtSql members including: + + * qtpy.QtSql.QSqlDatabase.exec_ + * qtpy.QtSql.QSqlQuery.exec_ + * qtpy.QtSql.QSqlResult.exec_ + """ + assert QtSql.QSqlDatabase.exec_ is not None + assert QtSql.QSqlQuery.exec_ is not None + assert QtSql.QSqlResult.exec_ is not None + + assert database_connection.open() + database_connection.setDatabaseName("test.sqlite") + QtSql.QSqlDatabase.exec_( + database_connection, + """ + CREATE TABLE test ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(40) NOT NULL + ) + """, + ) + + # Created table 'test' and 'sqlite_sequence' + assert len(database_connection.tables()) == 2 + + insert_table_query = QtSql.QSqlQuery() + assert insert_table_query.exec_( + """ + INSERT INTO test (name) VALUES ( + "TESTING" + ) + """, + ) + + select_table_query = QtSql.QSqlQuery() + select_table_query.prepare( + """ + SELECT * FROM test + """, + ) + select_table_query.exec_() + record = select_table_query.record() + assert not record.isEmpty() + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + assert ( + QtSql.QSqlDriver.MSSqlServer == QtSql.QSqlDriver.DbmsType.MSSqlServer + ) diff --git a/tests/test_qtstatemachine.py b/tests/test_qtstatemachine.py new file mode 100644 index 00000000..33c20b67 --- /dev/null +++ b/tests/test_qtstatemachine.py @@ -0,0 +1,52 @@ +import pytest + +from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE6 +from tests.utils import using_conda + + +@pytest.mark.skipif( + PYQT6 and int(PYQT_VERSION.split(".")[1]) < 9, + reason="QtStateMachine has been added to PyQt6 in version 6.9", +) +@pytest.mark.skipif( + PYSIDE6 and using_conda(), + reason="Not available on PySide6 with conda", +) +def test_qtstatemachine(): + """Test the qtpy.QtStateMachine namespace""" + from qtpy import QtStateMachine + + assert QtStateMachine.QAbstractState is not None + assert QtStateMachine.QAbstractTransition is not None + assert QtStateMachine.QEventTransition is not None + assert QtStateMachine.QFinalState is not None + assert QtStateMachine.QHistoryState is not None + assert QtStateMachine.QKeyEventTransition is not None + assert QtStateMachine.QMouseEventTransition is not None + assert QtStateMachine.QSignalTransition is not None + assert QtStateMachine.QState is not None + + +@pytest.mark.skipif( + PYQT6 and int(PYQT_VERSION.split(".")[1]) < 9, + reason="QtStateMachine has been added to PyQt6 in version 6.9", +) +@pytest.mark.skipif( + PYSIDE6 and using_conda(), + reason="Not available on PySide6 with conda", +) +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + from qtpy import QtStateMachine + + assert ( + QtStateMachine.QAbstractTransition.ExternalTransition + == QtStateMachine.QAbstractTransition.TransitionType.ExternalTransition + ) diff --git a/tests/test_qtsvg.py b/tests/test_qtsvg.py new file mode 100644 index 00000000..c39c95f4 --- /dev/null +++ b/tests/test_qtsvg.py @@ -0,0 +1,14 @@ +import pytest + +from qtpy import PYQT6, PYSIDE6 + + +def test_qtsvg(): + """Test the qtpy.QtSvg namespace""" + QtSvg = pytest.importorskip("qtpy.QtSvg") + + if not (PYSIDE6 or PYQT6): + assert QtSvg.QGraphicsSvgItem is not None + assert QtSvg.QSvgWidget is not None + assert QtSvg.QSvgGenerator is not None + assert QtSvg.QSvgRenderer is not None diff --git a/tests/test_qtsvgwidgets.py b/tests/test_qtsvgwidgets.py new file mode 100644 index 00000000..ac7d5b07 --- /dev/null +++ b/tests/test_qtsvgwidgets.py @@ -0,0 +1,9 @@ +from tests.utils import pytest_importorskip + + +def test_qtsvgwidgets(): + """Test the qtpy.QtSvgWidgets namespace""" + QtSvgWidgets = pytest_importorskip("qtpy.QtSvgWidgets") + + assert QtSvgWidgets.QGraphicsSvgItem is not None + assert QtSvgWidgets.QSvgWidget is not None diff --git a/tests/test_qttest.py b/tests/test_qttest.py new file mode 100644 index 00000000..13b647f6 --- /dev/null +++ b/tests/test_qttest.py @@ -0,0 +1,28 @@ +import pytest + +from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE6, QtTest, _parse_version + + +def test_qttest(): + """Test the qtpy.QtTest namespace""" + assert QtTest.QTest is not None + + if PYQT5 or PYQT6 or PYSIDE6: + assert QtTest.QSignalSpy is not None + + if ( + (PYQT5 and _parse_version(PYQT_VERSION) >= _parse_version("5.11")) + or PYQT6 + or PYSIDE6 + ): + assert QtTest.QAbstractItemModelTester is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access", +) +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtTest.*.""" + assert QtTest.QTest.Click == QtTest.QTest.KeyAction.Click diff --git a/tests/test_qttexttospeech.py b/tests/test_qttexttospeech.py new file mode 100644 index 00000000..ae82eeaf --- /dev/null +++ b/tests/test_qttexttospeech.py @@ -0,0 +1,21 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION, PYSIDE2, _parse_version + + +@pytest.mark.skipif( + not ( + (PYQT5 and _parse_version(PYQT_VERSION) >= _parse_version("5.15.1")) + or PYSIDE2 + ), + reason="Only available in Qt5 bindings (PyQt5 >= 5.15.1 or PySide2)", +) +def test_qttexttospeech(): + """Test the qtpy.QtTextToSpeech namespace.""" + from qtpy import QtTextToSpeech + + assert QtTextToSpeech.QTextToSpeech is not None + assert QtTextToSpeech.QVoice is not None + + if PYSIDE2: + assert QtTextToSpeech.QTextToSpeechEngine is not None diff --git a/tests/test_qtuitools.py b/tests/test_qtuitools.py new file mode 100644 index 00000000..d4b3b830 --- /dev/null +++ b/tests/test_qtuitools.py @@ -0,0 +1,8 @@ +from tests.utils import pytest_importorskip + + +def test_qtuitools(): + """Test the qtpy.QtUiTools namespace""" + QtUiTools = pytest_importorskip("qtpy.QtUiTools") + + assert QtUiTools.QUiLoader is not None diff --git a/tests/test_qtwebchannel.py b/tests/test_qtwebchannel.py new file mode 100644 index 00000000..8a364bae --- /dev/null +++ b/tests/test_qtwebchannel.py @@ -0,0 +1,6 @@ +def test_qtwebchannel(): + """Test the qtpy.QtWebChannel namespace""" + from qtpy import QtWebChannel + + assert QtWebChannel.QWebChannel is not None + assert QtWebChannel.QWebChannelAbstractTransport is not None diff --git a/tests/test_qtwebenginecore.py b/tests/test_qtwebenginecore.py new file mode 100644 index 00000000..6560b193 --- /dev/null +++ b/tests/test_qtwebenginecore.py @@ -0,0 +1,28 @@ +import pytest + +from qtpy import PYQT5, PYQT_VERSION +from tests.utils import pytest_importorskip + + +def test_qtwebenginecore(): + """Test the qtpy.QtWebEngineCore namespace""" + QtWebEngineCore = pytest_importorskip("qtpy.QtWebEngineCore") + + assert QtWebEngineCore.QWebEngineHttpRequest is not None + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason=( + "A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.* " + "to work with scoped enum access" + ), +) +def test_enum_access(): + """Test scoped and unscoped enum access.""" + QtWebEngineCore = pytest_importorskip("qtpy.QtWebEngineCore") + + assert ( + QtWebEngineCore.QWebEngineHttpRequest.Get + == QtWebEngineCore.QWebEngineHttpRequest.Method.Get + ) diff --git a/tests/test_qtwebenginequick.py b/tests/test_qtwebenginequick.py new file mode 100644 index 00000000..ed24cf49 --- /dev/null +++ b/tests/test_qtwebenginequick.py @@ -0,0 +1,26 @@ +import pytest + +from qtpy import PYQT5, PYSIDE2 +from tests.utils import pytest_importorskip, using_conda + + +@pytest.mark.skipif(not using_conda(), reason="Fails with pip packages") +@pytest.mark.skipif(PYQT5 or PYSIDE2, reason="Only available in Qt6 bindings") +def test_qtwebenginequick(): + """Test the qtpy.QtWebEngineQuick namespace""" + QtWebEngineQuick = pytest_importorskip("qtpy.QtWebEngineQuick") + + assert QtWebEngineQuick.QtWebEngineQuick is not None + assert QtWebEngineQuick.QQuickWebEngineProfile is not None + + +@pytest.mark.skipif(not using_conda(), reason="Fails with pip packages") +@pytest.mark.skipif(PYQT5 or PYSIDE2, reason="Only available in Qt6 bindings") +def test_enum_access(): + """Test scoped and unscoped enum access.""" + QtWebEngineQuick = pytest_importorskip("qtpy.QtWebEngineQuick") + + assert ( + QtWebEngineQuick.QQuickWebEngineProfile.NoPersistentCookies + == QtWebEngineQuick.QQuickWebEngineProfile.PersistentCookiesPolicy.NoPersistentCookies + ) diff --git a/tests/test_qtwebenginewidgets.py b/tests/test_qtwebenginewidgets.py new file mode 100644 index 00000000..df527bc5 --- /dev/null +++ b/tests/test_qtwebenginewidgets.py @@ -0,0 +1,34 @@ +import pytest + +from qtpy import ( + PYQT5, + PYQT6, + PYQT_VERSION, + PYSIDE2, + PYSIDE6, + PYSIDE_VERSION, + _parse_version, +) +from tests.utils import pytest_importorskip + + +@pytest.mark.skipif( + not ( + (PYQT6 and _parse_version(PYQT_VERSION) >= _parse_version("6.2")) + or ( + PYSIDE6 and _parse_version(PYSIDE_VERSION) >= _parse_version("6.2") + ) + or PYQT5 + or PYSIDE2 + ), + reason="Only available in Qt<6,>=6.2 bindings", +) +def test_qtwebenginewidgets(): + """Test the qtpy.QtWebEngineWidget namespace""" + + QtWebEngineWidgets = pytest_importorskip("qtpy.QtWebEngineWidgets") + + assert QtWebEngineWidgets.QWebEnginePage is not None + assert QtWebEngineWidgets.QWebEngineView is not None + assert QtWebEngineWidgets.QWebEngineSettings is not None + assert QtWebEngineWidgets.QWebEngineScript is not None diff --git a/tests/test_qtwebsockets.py b/tests/test_qtwebsockets.py new file mode 100644 index 00000000..ae169023 --- /dev/null +++ b/tests/test_qtwebsockets.py @@ -0,0 +1,9 @@ +def test_qtwebsockets(): + """Test the qtpy.QtWebSockets namespace""" + from qtpy import QtWebSockets + + assert QtWebSockets.QMaskGenerator is not None + assert QtWebSockets.QWebSocket is not None + assert QtWebSockets.QWebSocketCorsAuthenticator is not None + assert QtWebSockets.QWebSocketProtocol is not None + assert QtWebSockets.QWebSocketServer is not None diff --git a/tests/test_qtwidgets.py b/tests/test_qtwidgets.py new file mode 100644 index 00000000..00e242d9 --- /dev/null +++ b/tests/test_qtwidgets.py @@ -0,0 +1,303 @@ +"""Test QtWidgets.""" + +import sys +from time import sleep + +import pytest + +from qtpy import ( + PYQT5, + PYQT_VERSION, + PYSIDE2, + QtCore, + QtGui, + QtWidgets, +) + + +def test_qtextedit_functions(qtbot, pdf_writer): + """Test functions mapping for QtWidgets.QTextEdit.""" + assert QtWidgets.QTextEdit.setTabStopWidth + assert QtWidgets.QTextEdit.tabStopWidth + assert QtWidgets.QTextEdit.print_ + textedit_widget = QtWidgets.QTextEdit(None) + textedit_widget.setTabStopWidth(90) + assert textedit_widget.tabStopWidth() == 90 + print_device, output_path = pdf_writer + textedit_widget.print_(print_device) + assert output_path.exists() + + +def test_qlineedit_functions(): + """Test functions mapping for QtWidgets.QLineEdit""" + assert QtWidgets.QLineEdit.getTextMargins + + +def test_what_moved_to_qtgui_in_qt6(): + """Test that we move back what has been moved to QtGui in Qt6""" + assert QtWidgets.QAction is not None + assert QtWidgets.QActionGroup is not None + assert QtWidgets.QFileSystemModel is not None + assert QtWidgets.QShortcut is not None + assert QtWidgets.QUndoCommand is not None + assert QtWidgets.QUndoStack is not None + + +def test_qplaintextedit_functions(qtbot, pdf_writer): + """Test functions mapping for QtWidgets.QPlainTextEdit.""" + assert QtWidgets.QPlainTextEdit.setTabStopWidth + assert QtWidgets.QPlainTextEdit.tabStopWidth + assert QtWidgets.QPlainTextEdit.print_ + plaintextedit_widget = QtWidgets.QPlainTextEdit(None) + plaintextedit_widget.setTabStopWidth(90) + assert plaintextedit_widget.tabStopWidth() == 90 + print_device, output_path = pdf_writer + plaintextedit_widget.print_(print_device) + assert output_path.exists() + + +def test_QApplication_exec_(): + """Test `QtWidgets.QApplication.exec_`""" + assert QtWidgets.QApplication.exec_ is not None + app = QtWidgets.QApplication.instance() or QtWidgets.QApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtWidgets.QApplication.instance().quit) + QtWidgets.QApplication.exec_() + app = QtWidgets.QApplication.instance() or QtWidgets.QApplication( + [sys.executable, __file__], + ) + assert app is not None + QtCore.QTimer.singleShot(100, QtWidgets.QApplication.instance().quit) + app.exec_() + + +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +def test_qdialog_functions(qtbot): + """Test functions mapping for QtWidgets.QDialog.""" + assert QtWidgets.QDialog.exec_ + dialog = QtWidgets.QDialog(None) + QtCore.QTimer.singleShot(100, dialog.accept) + dialog.exec_() + + +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +def test_qdialog_subclass(qtbot): + """Test functions mapping for QtWidgets.QDialog when using a subclass""" + assert QtWidgets.QDialog.exec_ + + class CustomDialog(QtWidgets.QDialog): + def __init__(self): + super().__init__(None) + self.setWindowTitle("Testing") + + assert CustomDialog.exec_ + dialog = CustomDialog() + QtCore.QTimer.singleShot(100, dialog.accept) + dialog.exec_() + + +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +def test_QMenu_functions(qtbot): + """Test functions mapping for `QtWidgets.QMenu`.""" + # A window is required for static calls + window = QtWidgets.QMainWindow() + menu = QtWidgets.QMenu(window) + menu.addAction("QtPy") + menu.addAction("QtPy with a Qt.Key shortcut", QtCore.Qt.Key_F1) + menu.addAction( + QtGui.QIcon(), + "QtPy with an icon and a QKeySequence shortcut", + QtGui.QKeySequence.UnknownKey, + ) + window.show() + + with qtbot.waitExposed(window): + # Call `exec_` of a `QMenu` instance + QtCore.QTimer.singleShot(100, menu.close) + menu.exec_() + + # Call static `QMenu.exec_` + QtCore.QTimer.singleShot( + 100, + lambda: qtbot.keyClick( + QtWidgets.QApplication.widgetAt(1, 1), + QtCore.Qt.Key_Escape, + ), + ) + QtWidgets.QMenu.exec_(menu.actions(), QtCore.QPoint(1, 1)) + + +def test_QMenu_instance(qtbot): + """Test `QtWidgets.QMenu` submenus are `QtWidgets.QMenu` instances.""" + menu = QtWidgets.QMenu() + menu.addMenu("test") + submenu = menu.children()[1] + assert isinstance(submenu, QtWidgets.QMenu) + + +@pytest.mark.skipif( + sys.platform == "darwin" and sys.version_info[:2] == (3, 7), + reason="Stalls on macOS CI with Python 3.7", +) +def test_QToolBar_functions(qtbot): + """Test `QtWidgets.QToolBar.addAction` compatibility with Qt6 arguments' order.""" + toolbar = QtWidgets.QToolBar() + toolbar.addAction("QtPy with a shortcut", QtCore.Qt.Key_F1) + toolbar.addAction( + QtGui.QIcon(), + "QtPy with an icon and a shortcut", + QtGui.QKeySequence.UnknownKey, + ) + + +def test_QToolBar_instance(qtbot): + """Test `QtWidgets.QToolBar` passes `isinstance` checks.""" + window = QtWidgets.QMainWindow() + new_toolbar = window.addToolBar("Toolbar title") + assert isinstance(new_toolbar, QtWidgets.QToolBar) + + +@pytest.mark.skipif( + PYQT5 and PYQT_VERSION.startswith("5.9"), + reason="A specific setup with at least sip 4.9.9 is needed for PyQt5 5.9.*" + "to work with scoped enum access", +) +def test_enum_access(): + """Test scoped and unscoped enum access for qtpy.QtWidgets.*.""" + assert ( + QtWidgets.QFileDialog.AcceptOpen + == QtWidgets.QFileDialog.AcceptMode.AcceptOpen + ) + assert ( + QtWidgets.QMessageBox.InvalidRole + == QtWidgets.QMessageBox.ButtonRole.InvalidRole + ) + assert QtWidgets.QStyle.State_None == QtWidgets.QStyle.StateFlag.State_None + assert ( + QtWidgets.QSlider.TicksLeft + == QtWidgets.QSlider.TickPosition.TicksAbove + ) + assert ( + QtWidgets.QStyle.SC_SliderGroove + == QtWidgets.QStyle.SubControl.SC_SliderGroove + ) + + +def test_opengl_imports(): + """ + Test for presence of QOpenGLWidget. + + QOpenGLWidget was a member of QtWidgets in Qt5, but moved to QtOpenGLWidgets in Qt6. + QtPy makes QOpenGLWidget available in QtWidgets to maintain compatibility. + """ + assert QtWidgets.QOpenGLWidget is not None + + +@pytest.mark.skipif( + sys.platform == "darwin" + and sys.version_info[:2] == (3, 7) + and (PYQT5 or PYSIDE2), + reason="Crashes on macOS with Python 3.7 with 'Illegal instruction: 4'", +) +@pytest.mark.parametrize("keyword", ["dir", "directory"]) +@pytest.mark.parametrize("instance", [True, False]) +def test_qfiledialog_dir_compat(tmp_path, qtbot, keyword, instance): + """ + This function is testing if the decorators that renamed the dir/directory + keyword are working. + + It may stop working if the Qt bindings do some overwriting of the methods + in constructor. It should not happen, but the PySide team + did similar things in the past (like overwriting enum module in + PySide6==6.3.2). + + keyword: str + The keyword that should be used in the function call. + instance: bool + If True, the function is called on the instance of the QFileDialog, + otherwise on the class. + """ + + class CloseThread(QtCore.QThread): + """ + On some implementations the `getExistingDirectory` functions starts own + event loop that will not trigger QTimer started before the call. Until + the dialog is closed the main event loop will be stopped. + + Because of this it is required to use the thread to interact with the + dialog. + """ + + def run(self, allow_restart=True): + sleep(0.5) + need_restart = allow_restart + app = QtWidgets.QApplication.instance() + for dlg in app.topLevelWidgets(): + if ( + not isinstance(dlg, QtWidgets.QFileDialog) + or dlg.isHidden() + ): + continue + # "when implemented this I try to use: + # * dlg.close() - On Qt6 it will close the dialog, but it will + # not restart the main event loop. + # * dlg.accept() - It ends with information thar `accept` and + # `reject` of such created dialog can not be called. + # * accept dialog with enter - It works, but it cannot be + # called to early after dialog is shown + qtbot.keyClick(dlg, QtCore.Qt.Key_Enter) + need_restart = False + sleep(0.1) + for dlg in app.topLevelWidgets(): + # As described above, it may happen that dialog is not closed after first using enter. + # in such case we call `run` function again. The 0.5s sleep is enough for the second enter to close the dialog. + if ( + not isinstance(dlg, QtWidgets.QFileDialog) + or dlg.isHidden() + ): + continue + self.run(allow_restart=False) + return + + if need_restart: + self.run() + + # We need to use the `DontUseNativeDialog` option to be able to interact + # with it from code. + try: + opt = QtWidgets.QFileDialog.Option.DontUseNativeDialog + except AttributeError: + # old qt5 bindings + opt = QtWidgets.QFileDialog.DontUseNativeDialog + + kwargs = { + "caption": "Select a directory", + keyword: str(tmp_path), + "options": opt, + } + + thr = CloseThread() + thr.start() + qtbot.waitUntil(thr.isRunning, timeout=1000) + dlg = QtWidgets.QFileDialog() if instance else QtWidgets.QFileDialog + dlg.getExistingDirectory(**kwargs) + qtbot.waitUntil(thr.isFinished, timeout=3000) + + +def test_qfiledialog_flags_typedef(): + """ + Test existence of `QFlags