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: Update README with information on Pyright/Pylance integration and add CLI subcommand to help generate the related config #450

Merged
merged 3 commits into from
Aug 24, 2023
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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ conda install qtpy

### Type checker integration

Type checkers have no knowledge of installed packages, so these tools require
additional configuration.

#### Mypy

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
Expand Down Expand Up @@ -132,7 +137,21 @@ the Mypy command line invocation as follows:
mypy --package mypackage $(qtpy mypy-args)
```

For Pyright support and other usage notes, see [this comment](https://github.com/spyder-ide/qtpy/issues/352#issuecomment-1170684412).
#### Pyright/Pylance

Instead of runtime arguments, it is required to create a config file for the project,
called `pyrightconfig.json` or a `pyright` section in `pyproject.toml`. See [here](https://github.com/microsoft/pyright/blob/main/docs/configuration.md) for reference.

If you run

```bash
qtpy pyright-config
```

you will get the necessary configs to be included in your project files. If you don't
have them, it is recommended to create the latter.

These steps are necessary for running the default VSCode's type checking.


## Contributing
Expand Down
81 changes: 73 additions & 8 deletions qtpy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Standard library imports
import argparse
import textwrap
import json


def print_version():
Expand All @@ -18,25 +19,66 @@ def print_version():
print('QtPy version', qtpy.__version__)


def get_api_status():
"""Get the status of each Qt API usage."""
import qtpy

return {name: qtpy.API == name 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'}

import qtpy

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


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(
Expand All @@ -46,10 +88,12 @@ def generate_arg_parser():

parser.add_argument(
'--version', action='store_const', dest='func', const=print_version,
help='If passed, will print the version and exit')
help='If passed, will print the version and exit',
)

cli_subparsers = parser.add_subparsers(
title='Subcommands', help='Subcommand to run', metavar='Subcommand')
title='Subcommands', help='Subcommand to run', metavar='Subcommand',
)

# Parser for the MyPy args subcommand
mypy_args_parser = cli_subparsers.add_parser(
Expand All @@ -69,11 +113,30 @@ def generate_arg_parser():
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


Expand All @@ -83,6 +146,8 @@ def main(args=None):
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}
cleaned_args = {
key: value for key, value in vars(parsed_args).items()
if key not in reserved_params
}
parsed_args.func(**cleaned_args)
62 changes: 62 additions & 0 deletions qtpy/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import subprocess
import sys
import textwrap

import pytest

Expand Down Expand Up @@ -75,3 +76,64 @@ def test_cli_mypy_args():
assert False, 'No valid API to test'

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:
assert False, 'No valid API to test'

assert output.stdout.strip() == expected.strip()
Loading