From 228108df91b41d2815084c71e20e8d1565d31e17 Mon Sep 17 00:00:00 2001 From: wkrasnicki Date: Thu, 24 Aug 2023 18:36:54 +0200 Subject: [PATCH 1/3] Add CLI option to generate Pyright config --- qtpy/cli.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/qtpy/cli.py b/qtpy/cli.py index 3a629970..0901bfd3 100644 --- a/qtpy/cli.py +++ b/qtpy/cli.py @@ -10,6 +10,7 @@ # Standard library imports import argparse import textwrap +import json def print_version(): @@ -18,13 +19,17 @@ 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() @@ -32,11 +37,44 @@ def generate_mypy_args(): 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()} = {is_active}" 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( @@ -46,10 +84,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( @@ -69,11 +109,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 @@ -83,6 +142,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) From 6e9983b71ac81ade074f208750b5953c637648c5 Mon Sep 17 00:00:00 2001 From: wkrasnicki Date: Thu, 24 Aug 2023 18:37:19 +0200 Subject: [PATCH 2/3] Update README on how to configure Pyright --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6781dbfb..c6fe1538 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 From 20c88e797d69a829a6753df9bf127f39f27ed8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Kra=C5=9Bnicki?= Date: Thu, 24 Aug 2023 21:34:57 +0200 Subject: [PATCH 3/3] Add tests and change toml boolean values to lowercase --- qtpy/cli.py | 8 ++++-- qtpy/tests/test_cli.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/qtpy/cli.py b/qtpy/cli.py index 0901bfd3..2fb57716 100644 --- a/qtpy/cli.py +++ b/qtpy/cli.py @@ -41,14 +41,18 @@ 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()}}) + 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()} = {is_active}" for name, is_active in apis_active.items()) + 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(): diff --git a/qtpy/tests/test_cli.py b/qtpy/tests/test_cli.py index 6b0f0381..6b3efe3f 100644 --- a/qtpy/tests/test_cli.py +++ b/qtpy/tests/test_cli.py @@ -2,6 +2,7 @@ import subprocess import sys +import textwrap import pytest @@ -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()