Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add command line support for Mypy #337

Merged
merged 7 commits into from
Apr 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
dalthviz marked this conversation as resolved.
Show resolved Hide resolved

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
dalthviz marked this conversation as resolved.
Show resolved Hide resolved


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