Skip to content

Commit 8017a8f

Browse files
authored
PR: Update Docs with information on Pyright/Pylance integration and add CLI subcommand to help generate the related config (#450)
2 parents 6ae5759 + 20c88e7 commit 8017a8f

File tree

3 files changed

+155
-9
lines changed

3 files changed

+155
-9
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ conda install qtpy
104104

105105
### Type checker integration
106106

107+
Type checkers have no knowledge of installed packages, so these tools require
108+
additional configuration.
109+
110+
#### Mypy
111+
107112
A Command Line Interface (CLI) is offered to help with usage of QtPy.
108113
Presently, its only feature is to generate command line arguments for Mypy
109114
that will enable it to process the QtPy source files with the same API
@@ -132,7 +137,21 @@ the Mypy command line invocation as follows:
132137
mypy --package mypackage $(qtpy mypy-args)
133138
```
134139

135-
For Pyright support and other usage notes, see [this comment](https://github.com/spyder-ide/qtpy/issues/352#issuecomment-1170684412).
140+
#### Pyright/Pylance
141+
142+
Instead of runtime arguments, it is required to create a config file for the project,
143+
called `pyrightconfig.json` or a `pyright` section in `pyproject.toml`. See [here](https://github.com/microsoft/pyright/blob/main/docs/configuration.md) for reference.
144+
145+
If you run
146+
147+
```bash
148+
qtpy pyright-config
149+
```
150+
151+
you will get the necessary configs to be included in your project files. If you don't
152+
have them, it is recommended to create the latter.
153+
154+
These steps are necessary for running the default VSCode's type checking.
136155

137156

138157
## Contributing

qtpy/cli.py

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# Standard library imports
1111
import argparse
1212
import textwrap
13+
import json
1314

1415

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

2021

22+
def get_api_status():
23+
"""Get the status of each Qt API usage."""
24+
import qtpy
25+
26+
return {name: qtpy.API == name for name in qtpy.API_NAMES}
27+
2128
def generate_mypy_args():
2229
"""Generate a string with always-true/false args to pass to mypy."""
2330
options = {False: '--always-false', True: '--always-true'}
2431

25-
import qtpy
26-
27-
apis_active = {name: qtpy.API == name for name in qtpy.API_NAMES}
32+
apis_active = get_api_status()
2833
mypy_args = ' '.join(
2934
f'{options[is_active]}={name.upper()}'
3035
for name, is_active in apis_active.items()
3136
)
3237
return mypy_args
3338

3439

40+
def generate_pyright_config_json():
41+
"""Generate Pyright config to be used in `pyrightconfig.json`."""
42+
apis_active = get_api_status()
43+
44+
return json.dumps({
45+
"defineConstant": {name.upper(): is_active for name, is_active in apis_active.items()}
46+
})
47+
48+
49+
def generate_pyright_config_toml():
50+
"""Generate a Pyright config to be used in `pyproject.toml`."""
51+
apis_active = get_api_status()
52+
53+
return "[tool.pyright.defineConstant]\n" + "\n".join(
54+
f"{name.upper()} = {str(is_active).lower()}" for name, is_active in apis_active.items()
55+
)
56+
57+
3558
def print_mypy_args():
3659
"""Print the generated mypy args to stdout."""
3760
print(generate_mypy_args())
3861

3962

63+
def print_pyright_config_json():
64+
"""Print the generated Pyright JSON config to stdout."""
65+
print(generate_pyright_config_json())
66+
67+
68+
def print_pyright_config_toml():
69+
"""Print the generated Pyright TOML config to stdout."""
70+
print(generate_pyright_config_toml())
71+
72+
73+
def print_pyright_configs():
74+
"""Print the generated Pyright configs to stdout."""
75+
print("pyrightconfig.json:")
76+
print_pyright_config_json()
77+
print()
78+
print("pyproject.toml:")
79+
print_pyright_config_toml()
80+
81+
4082
def generate_arg_parser():
4183
"""Generate the argument parser for the dev CLI for QtPy."""
4284
parser = argparse.ArgumentParser(
@@ -46,10 +88,12 @@ def generate_arg_parser():
4688

4789
parser.add_argument(
4890
'--version', action='store_const', dest='func', const=print_version,
49-
help='If passed, will print the version and exit')
91+
help='If passed, will print the version and exit',
92+
)
5093

5194
cli_subparsers = parser.add_subparsers(
52-
title='Subcommands', help='Subcommand to run', metavar='Subcommand')
95+
title='Subcommands', help='Subcommand to run', metavar='Subcommand',
96+
)
5397

5498
# Parser for the MyPy args subcommand
5599
mypy_args_parser = cli_subparsers.add_parser(
@@ -69,11 +113,30 @@ def generate_arg_parser():
69113
It can be used as follows on Bash or a similar shell:
70114
71115
mypy --package mypackage $(qtpy mypy-args)
72-
"""
116+
""",
73117
),
74118
)
75119
mypy_args_parser.set_defaults(func=print_mypy_args)
76120

121+
# Parser for the Pyright config subcommand
122+
pyright_config_parser = cli_subparsers.add_parser(
123+
name='pyright-config',
124+
help='Generate Pyright config for using Pyright with QtPy.',
125+
formatter_class=argparse.RawTextHelpFormatter,
126+
description=textwrap.dedent(
127+
"""
128+
Generate Pyright config for using Pyright with QtPy.
129+
130+
This will generate config sections to be included in a Pyright
131+
config file (either `pyrightconfig.json` or `pyproject.toml`)
132+
which help guide Pyright through which library QtPy would have used
133+
so that Pyright can get the proper underlying type hints.
134+
135+
""",
136+
),
137+
)
138+
pyright_config_parser.set_defaults(func=print_pyright_configs)
139+
77140
return parser
78141

79142

@@ -83,6 +146,8 @@ def main(args=None):
83146
parsed_args = parser.parse_args(args=args)
84147

85148
reserved_params = {'func'}
86-
cleaned_args = {key: value for key, value in vars(parsed_args).items()
87-
if key not in reserved_params}
149+
cleaned_args = {
150+
key: value for key, value in vars(parsed_args).items()
151+
if key not in reserved_params
152+
}
88153
parsed_args.func(**cleaned_args)

qtpy/tests/test_cli.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import subprocess
44
import sys
5+
import textwrap
56

67
import pytest
78

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

7778
assert output.stdout.strip() == expected.strip()
79+
80+
def test_cli_pyright_config():
81+
output = subprocess.run(
82+
[sys.executable, '-m', 'qtpy', 'pyright-config'],
83+
capture_output=True,
84+
check=True,
85+
encoding='utf-8',
86+
)
87+
88+
if qtpy.PYQT5:
89+
expected = textwrap.dedent("""
90+
pyrightconfig.json:
91+
{"defineConstant": {"PYQT5": true, "PYSIDE2": false, "PYQT6": false, "PYSIDE6": false}}
92+
93+
pyproject.toml:
94+
[tool.pyright.defineConstant]
95+
PYQT5 = true
96+
PYSIDE2 = false
97+
PYQT6 = false
98+
PYSIDE6 = false
99+
""")
100+
elif qtpy.PYSIDE2:
101+
expected = textwrap.dedent("""
102+
pyrightconfig.json:
103+
{"defineConstant": {"PYQT5": false, "PYSIDE2": true, "PYQT6": false, "PYSIDE6": false}}
104+
105+
pyproject.toml:
106+
[tool.pyright.defineConstant]
107+
PYQT5 = false
108+
PYSIDE2 = true
109+
PYQT6 = false
110+
PYSIDE6 = false
111+
""")
112+
elif qtpy.PYQT6:
113+
expected = textwrap.dedent("""
114+
pyrightconfig.json:
115+
{"defineConstant": {"PYQT5": false, "PYSIDE2": false, "PYQT6": true, "PYSIDE6": false}}
116+
117+
pyproject.toml:
118+
[tool.pyright.defineConstant]
119+
PYQT5 = false
120+
PYSIDE2 = false
121+
PYQT6 = true
122+
PYSIDE6 = false
123+
""")
124+
elif qtpy.PYSIDE6:
125+
expected = textwrap.dedent("""
126+
pyrightconfig.json:
127+
{"defineConstant": {"PYQT5": false, "PYSIDE2": false, "PYQT6": false, "PYSIDE6": true}}
128+
129+
pyproject.toml:
130+
[tool.pyright.defineConstant]
131+
PYQT5 = false
132+
PYSIDE2 = false
133+
PYQT6 = false
134+
PYSIDE6 = true
135+
""")
136+
else:
137+
assert False, 'No valid API to test'
138+
139+
assert output.stdout.strip() == expected.strip()

0 commit comments

Comments
 (0)