diff --git a/MANIFEST.in b/MANIFEST.in index c089429f..3c312e0c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,3 +5,4 @@ include README* include SECURITY* include pytest.ini recursive-include qtpy/tests *.py *.ui +include qtpy/py.typed diff --git a/README.md b/README.md index bd272131..a752f5df 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,25 @@ conda install qtpy ``` +### mypy + +A CLI is offered to help with usage of QtPy. Presently, the only feature +is to generate 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. + +``` +--always-false=PYQT5 --always-false=PYQT6 --always-true=PYSIDE2 --always-false=PYSIDE6 +``` + +If using bash or similar, this can be integrated into the Mypy command line +as follows. + +```console +$ env/bin/mypy --package mypackage $(env/bin/qtpy mypy-args) +``` + + ## Contributing Everyone is welcome to contribute! diff --git a/qtpy/__init__.py b/qtpy/__init__.py index f9a020b6..f4fd0b6d 100644 --- a/qtpy/__init__.py +++ b/qtpy/__init__.py @@ -101,9 +101,11 @@ class PythonQtWarning(Warning): # Setting a default value for QT_API os.environ.setdefault(QT_API, 'pyqt5') +API_NAMES = {'pyqt5': 'PyQt5', 'pyqt6': 'PyQt6', + 'pyside2':'PySide2', 'pyside6': 'PySide6'} API = os.environ[QT_API].lower() initial_api = API -assert API in (PYQT5_API + PYQT6_API + PYSIDE2_API + PYSIDE6_API) +assert API in API_NAMES is_old_pyqt = is_pyqt46 = False QT5 = PYQT5 = True @@ -201,8 +203,9 @@ class PythonQtWarning(Warning): warnings.warn('Selected binding "{}" could not be found, ' 'using "{}"'.format(initial_api, API), RuntimeWarning) -API_NAME = {'pyqt6': 'PyQt6', 'pyqt5': 'PyQt5', - 'pyside2':'PySide2', 'pyside6': 'PySide6'}[API] + +# Set display name of the Qt API +API_NAME = API_NAMES[API] try: # QtDataVisualization backward compatibility (QtDataVisualization vs. QtDatavisualization) diff --git a/qtpy/__main__.py b/qtpy/__main__.py new file mode 100644 index 00000000..f4bc9027 --- /dev/null +++ b/qtpy/__main__.py @@ -0,0 +1,9 @@ +import qtpy.cli + + +def main(): + return qtpy.cli.cli() + + +if __name__ == "__main__": + main() diff --git a/qtpy/cli.py b/qtpy/cli.py new file mode 100644 index 00000000..1a289758 --- /dev/null +++ b/qtpy/cli.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# 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 sys +import textwrap + + +class RawDescriptionArgumentDefaultsHelpFormatter( + argparse.RawDescriptionHelpFormatter, + argparse.ArgumentDefaultsHelpFormatter, +): + pass + + +def cli(args=sys.argv[1:]): + parser = argparse.ArgumentParser( + description="Features in support of development with QtPy.", + formatter_class=RawDescriptionArgumentDefaultsHelpFormatter, + ) + + parser.set_defaults(func=parser.print_help) + + cli_subparsers = parser.add_subparsers() + + mypy_args_parser = cli_subparsers.add_parser( + name='mypy-args', + 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 + + Use such as: + + env/bin/mypy --package mypackage $(env/bin/qtpy mypy-args) + """ + ), + formatter_class=RawDescriptionArgumentDefaultsHelpFormatter, + ) + mypy_args_parser.set_defaults(func=mypy_args) + + arguments = parser.parse_args(args=args) + + reserved_parameters = {'func'} + cleaned = { + k: v + for k, v in vars(arguments).items() + if k not in reserved_parameters + } + + arguments.func(**cleaned) + + +def mypy_args(): + options = {False: '--always-false', True: '--always-true'} + + import qtpy + + apis_active = {name: qtpy.API == name for name in qtpy.API_NAMES} + print(' '.join( + f'{options[is_active]}={name.upper()}' + for name, is_active + in apis_active.items() + )) diff --git a/qtpy/py.typed b/qtpy/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/qtpy/tests/test_cli.py b/qtpy/tests/test_cli.py new file mode 100644 index 00000000..f89f2cb2 --- /dev/null +++ b/qtpy/tests/test_cli.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import + +import subprocess +import sys + +import pytest + +import qtpy + + +subcommands = [ + ['mypy'], + ['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): + # .check_call() over .run(..., check=True) because of py2 + subprocess.check_call( + [sys.executable, '-m', 'qtpy', *subcommand, '--help'], + ) + + +def test_cli_mypy_args(): + output = subprocess.check_output( + [sys.executable, '-m', 'qtpy', 'mypy', 'args'], + ) + + if qtpy.PYQT5: + expected = b'--always-true=PYQT5 --always-false=PYQT6 --always-false=PYSIDE2 --always-false=PYSIDE6\n' + elif qtpy.PYQT6: + expected = b'--always-false=PYQT5 --always-true=PYQT6 --always-false=PYSIDE2 --always-false=PYSIDE6\n' + elif qtpy.PYSIDE2: + expected = b'--always-false=PYQT5 --always-false=PYQT6 --always-true=PYSIDE2 --always-false=PYSIDE6\n' + elif qtpy.PYSIDE6: + expected = b'--always-false=PYQT5 --always-false=PYQT6 --always-false=PYSIDE2 --always-true=PYSIDE6\n' + else: + assert False, 'No valid API to test' + + assert output == expected diff --git a/setup.cfg b/setup.cfg index 1183a59a..63b88881 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,3 +56,7 @@ test = pytest>=6,!=7.0.0,!=7.0.1 pytest-cov>=3.0.0 pytest-qt + +[options.entry_points] +console_scripts = + qtpy = qtpy.__main__:main