Skip to content

Commit

Permalink
Merge pull request #337 from CAM-Gerlach/add-mypy-cli-v2
Browse files Browse the repository at this point in the history
PR: Add command line support for Mypy
  • Loading branch information
ccordoba12 authored Apr 23, 2022
2 parents 5c7b09b + cec7c82 commit 936e0c9
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 3 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ include README*
include SECURITY*
include pytest.ini
recursive-include qtpy/tests *.py *.ui
include qtpy/py.typed
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,37 @@ conda install qtpy
```


### Mypy integration

A Command Line Interface (CLI) is offered to help with usage of QtPy.
Presently, its 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.

If you run

```bash
qtpy mypy-args
```

QtPy will output a string of Mypy CLI args that will reflect the currently
selected Qt API.
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=PYQT6 --always-false=PYSIDE2 --always-false=PYSIDE6
```

Using Bash or a similar shell, this can be injected into
the Mypy command line invocation as follows:

```bash
mypy --package mypackage $(qtpy mypy-args)
```


## Contributing

Everyone is welcome to contribute!
Expand Down
9 changes: 6 additions & 3 deletions qtpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions qtpy/__main__.py
Original file line number Diff line number Diff line change
@@ -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()
88 changes: 88 additions & 0 deletions qtpy/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# -----------------------------------------------------------------------------
# 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 textwrap


def print_version():
"""Print the current version of the package."""
import qtpy
print('QtPy version', qtpy.__version__)


def generate_mypy_args():
"""Generate a string with always-true/false args to pass to mypy."""
options = {False: '--always-false', True: '--always-true'}

import qtpy

apis_active = {name: qtpy.API == name for name in qtpy.API_NAMES}
mypy_args = ' '.join(
f'{options[is_active]}={name.upper()}'
for name, is_active in apis_active.items()
)
return mypy_args


def print_mypy_args():
"""Print the generated mypy args to stdout."""
print(generate_mypy_args())


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)

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)
Empty file added qtpy/py.typed
Empty file.
77 changes: 77 additions & 0 deletions qtpy/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Test the QtPy CLI."""

import subprocess
import sys

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=PYQT6',
'--always-false=PYSIDE2',
'--always-false=PYSIDE6',
])
elif qtpy.PYQT6:
expected = ' '.join([
'--always-false=PYQT5',
'--always-true=PYQT6',
'--always-false=PYSIDE2',
'--always-false=PYSIDE6',
])
elif qtpy.PYSIDE2:
expected = ' '.join([
'--always-false=PYQT5',
'--always-false=PYQT6',
'--always-true=PYSIDE2',
'--always-false=PYSIDE6',
])
elif qtpy.PYSIDE6:
expected = ' '.join([
'--always-false=PYQT5',
'--always-false=PYQT6',
'--always-false=PYSIDE2',
'--always-true=PYSIDE6',
])
else:
assert False, 'No valid API to test'

assert output.stdout.strip() == expected.strip()
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 936e0c9

Please sign in to comment.