diff --git a/pyrightconfig.json b/pyrightconfig.json index e1674ad84..8724c34d5 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,7 +1,7 @@ // https://github.com/microsoft/pyright/blob/main/docs/configuration.md { "include": [ - "stubs", + "tests", "docs" ], "exclude": [ // other artefacts @@ -22,8 +22,8 @@ // ], "ignore": [ - "tests", - "docs", + // "tests", + // "docs", // see also : https://github.com/Josverl/micropython-stubs/issues/412 // webrepl "**/webrepl.py", diff --git a/requirements-test.txt b/requirements-test.txt index 6bf5821c0..d5e3c7088 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,9 +2,11 @@ micropython-stubber # for quality reporting -pyright==1.1.341 pytest +pyright==1.1.341 +mypy fasteners python-dotenv pydocstyle loguru +mypy_gitlab_code_quality diff --git a/scripts/try_mypy.ipynb b/scripts/try_mypy.ipynb index 676c8af42..eb2dc4dfd 100644 --- a/scripts/try_mypy.ipynb +++ b/scripts/try_mypy.ipynb @@ -64,6 +64,8 @@ } ], "source": [ + "# basic install with the latest local stdlib and esp32 stubs\n", + "\n", "import shutil\n", "\n", "if os.path.exists(check_path / \"typings\"):\n", @@ -76,10 +78,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ + "import subprocess\n", + "import json\n", + "from mypy_gitlab_code_quality import generate_report as gitlab_report\n", + "\n", "def mypy_patch(check_path):\n", " if not check_path.exists():\n", " Exception(f\"Path {check_path} not found\")\n", @@ -95,19 +101,6 @@ " elif file.is_dir():\n", " shutil.rmtree(file)\n", "\n", - " # else:\n", - "\n", - " # print(f\"File {f} not found\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "import subprocess\n", - "\n", "\n", "def run_mypy(path: Path):\n", " print(f\"Running mypy in {path}\")\n", @@ -125,73 +118,10 @@ " )\n", " return output.stdout\n", " except subprocess.CalledProcessError as e:\n", - " print(e)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running mypy in C:\\develop\\MyPython\\micropython-stubs\\tests\\quality_tests\\check_esp32\n" - ] - } - ], - "source": [ - "# run mypy\n", - "raw_results = run_mypy(check_path)\n", - "results = raw_results.split(\"\\n\")\n", - "# errors = []\n", - "# for i, line in enumerate(results):\n", - "# if \"error:\" in line:\n", - "# errors.append(line)\n", - "# elif \"warning\" in line:\n", - "# errors.append(line)\n", + " print(e)\n", "\n", - "# # print results\n", - "# for line in errors:\n", - "# print(line)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "# now parse to json and save\n", - "# https://github.com/python/mypy/issues/10816\n", - "\n", - "# mypy program.py | mypy-gitlab-code-quality" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "# # convert from mypy to gitlab code quality format\n", - "# from mypy_gitlab_code_quality import generate_report, parse_issue\n", - "\n", - "# gitlab_report = generate_report(map(str.rstrip, results))\n", - "\n", - "# gitlab_report" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ "# convert from gitlab to pyright format\n", "\n", - "import json\n", "HEADER = \"\"\"\n", "{\n", " \"version\": \"\",\n", @@ -245,9 +175,16 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 9, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running mypy in C:\\develop\\MyPython\\micropython-stubs\\tests\\quality_tests\\check_esp32\n" + ] + }, { "data": { "text/plain": [ @@ -266,13 +203,16 @@ " 'timeInSec': -1}}" ] }, - "execution_count": 15, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from mypy_gitlab_code_quality import generate_report as gitlab_report\n", + "# run mypy\n", + "raw_results = run_mypy(check_path)\n", + "results = raw_results.split(\"\\n\")\n", + "\n", "gitlab_to_pyright(gitlab_report(map(str.rstrip, results)))" ] }, diff --git a/tests/quality_tests/_configs/mypy.ini b/tests/quality_tests/_configs/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/_configs/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/_configs/pyrightconfig.json b/tests/quality_tests/_configs/pyrightconfig.json new file mode 100644 index 000000000..967c0766e --- /dev/null +++ b/tests/quality_tests/_configs/pyrightconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "." + ], + "typeshedPaths": [ + "./typings" + ], + "exclude": [ + ".*", + "__*", + "**/typings" + ], + "ignore": [ + "**/typings" + ], + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", + "verboseOutput": false, + "typeCheckingMode": "basic", + "typeshedPath": "./typings", + "reportMissingModuleSource": "none" +} \ No newline at end of file diff --git a/tests/quality_tests/_configs/readme.md b/tests/quality_tests/_configs/readme.md new file mode 100644 index 000000000..6ac99f407 --- /dev/null +++ b/tests/quality_tests/_configs/readme.md @@ -0,0 +1,3 @@ +# typechecker config files for tests + +The files in this folder are used copied to each check_* and feat_folder to make sure the typechecker tests that are all using the same configuration. diff --git a/tests/quality_tests/check_esp32/mypy.ini b/tests/quality_tests/check_esp32/mypy.ini index f24169474..f03131078 100644 --- a/tests/quality_tests/check_esp32/mypy.ini +++ b/tests/quality_tests/check_esp32/mypy.ini @@ -10,12 +10,12 @@ follow_imports = silent follow_imports_for_stubs = True no_site_packages = True -check_untyped_defs=True +check_untyped_defs = True ; show_error_context = True disable_error_code = no-redef,assignment -# sippets may re-use the same variable names +# snippets may re-use the same variable names allow_redefinition = True ; warn_return_any = True diff --git a/tests/quality_tests/check_esp8266/mypy.ini b/tests/quality_tests/check_esp8266/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_esp8266/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_esp8266/pyrightconfig.json b/tests/quality_tests/check_esp8266/pyrightconfig.json index addd551e7..967c0766e 100644 --- a/tests/quality_tests/check_esp8266/pyrightconfig.json +++ b/tests/quality_tests/check_esp8266/pyrightconfig.json @@ -11,10 +11,9 @@ "**/typings" ], "ignore": [ - "typings", "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/check_rp2-nano_connect/mypy.ini b/tests/quality_tests/check_rp2-nano_connect/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_rp2-nano_connect/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_rp2-nano_connect/pyrightconfig.json b/tests/quality_tests/check_rp2-nano_connect/pyrightconfig.json new file mode 100644 index 000000000..967c0766e --- /dev/null +++ b/tests/quality_tests/check_rp2-nano_connect/pyrightconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "." + ], + "typeshedPaths": [ + "./typings" + ], + "exclude": [ + ".*", + "__*", + "**/typings" + ], + "ignore": [ + "**/typings" + ], + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", + "verboseOutput": false, + "typeCheckingMode": "basic", + "typeshedPath": "./typings", + "reportMissingModuleSource": "none" +} \ No newline at end of file diff --git a/tests/quality_tests/check_rp2/check_machine/check_ds18x20.py b/tests/quality_tests/check_rp2/check_machine/check_ds18x20.py index 6e6a63523..153dd5251 100644 --- a/tests/quality_tests/check_rp2/check_machine/check_ds18x20.py +++ b/tests/quality_tests/check_rp2/check_machine/check_ds18x20.py @@ -2,7 +2,10 @@ import time import ds18x20 +import onewire +from machine import Pin +ow = onewire.OneWire(Pin(12)) ds = ds18x20.DS18X20(ow) roms = ds.scan() ds.convert_temp() diff --git a/tests/quality_tests/check_rp2/mypy.ini b/tests/quality_tests/check_rp2/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_rp2/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_rp2/pyrightconfig.json b/tests/quality_tests/check_rp2/pyrightconfig.json index 54df19d14..967c0766e 100644 --- a/tests/quality_tests/check_rp2/pyrightconfig.json +++ b/tests/quality_tests/check_rp2/pyrightconfig.json @@ -2,33 +2,21 @@ "include": [ "." ], + "typeshedPaths": [ + "./typings" + ], "exclude": [ ".*", "__*", - "typings" + "**/typings" + ], + "ignore": [ + "**/typings" ], - // section 1 - platform - "pythonVersion": "3.8", - "pythonPlatform": "Linux", // or "All" + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", "verboseOutput": false, - // section 2 - required settings - "typeCheckingMode": "basic", // ["off", "basic", "strict"]: Specifies the default rule set to use + "typeCheckingMode": "basic", "typeshedPath": "./typings", - // section 3 - set code quality checks - "reportMissingImports": "error", - "reportGeneralTypeIssues": "error", // interesting - "reportUndefinedVariable": "warning", // "_WHO_AM_I_REG" is not defined - Not a showstopper - // section 4 - informational - "reportMissingTypeStubs": "information", - "reportOptionalCall": "information", // Object of type "None" cannot be called - "reportInvalidStringEscapeSequence": "information", - "reportUnboundVariable": "information", - "ReportSelfClsParameterName": "information", - "reportOptionalSubscript": "information", - // section 5 - reduce noise - "reportOptionalMemberAccess": "none", // "read" is not a known member of "None" - occurs often in frozen code - "reportWildcardImportFromLibrary": "none", - "reportUnknownArgumentType": "none", - "reportSelfClsParameterName": "none", - "reportMissingModuleSource": "none", + "reportMissingModuleSource": "none" } \ No newline at end of file diff --git a/tests/quality_tests/check_samd/mypy.ini b/tests/quality_tests/check_samd/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_samd/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_samd/pyrightconfig.json b/tests/quality_tests/check_samd/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/check_samd/pyrightconfig.json +++ b/tests/quality_tests/check_samd/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/check_stm32/mypy.ini b/tests/quality_tests/check_stm32/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_stm32/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_stm32/pyrightconfig.json b/tests/quality_tests/check_stm32/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/check_stm32/pyrightconfig.json +++ b/tests/quality_tests/check_stm32/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/check_uasynio/mypy.ini b/tests/quality_tests/check_uasynio/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_uasynio/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_uasynio/pyrightconfig.json b/tests/quality_tests/check_uasynio/pyrightconfig.json new file mode 100644 index 000000000..967c0766e --- /dev/null +++ b/tests/quality_tests/check_uasynio/pyrightconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "." + ], + "typeshedPaths": [ + "./typings" + ], + "exclude": [ + ".*", + "__*", + "**/typings" + ], + "ignore": [ + "**/typings" + ], + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", + "verboseOutput": false, + "typeCheckingMode": "basic", + "typeshedPath": "./typings", + "reportMissingModuleSource": "none" +} \ No newline at end of file diff --git a/tests/quality_tests/check_unix/mypy.ini b/tests/quality_tests/check_unix/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_unix/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_unix/pyrightconfig.json b/tests/quality_tests/check_unix/pyrightconfig.json index 54df19d14..967c0766e 100644 --- a/tests/quality_tests/check_unix/pyrightconfig.json +++ b/tests/quality_tests/check_unix/pyrightconfig.json @@ -2,33 +2,21 @@ "include": [ "." ], + "typeshedPaths": [ + "./typings" + ], "exclude": [ ".*", "__*", - "typings" + "**/typings" + ], + "ignore": [ + "**/typings" ], - // section 1 - platform - "pythonVersion": "3.8", - "pythonPlatform": "Linux", // or "All" + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", "verboseOutput": false, - // section 2 - required settings - "typeCheckingMode": "basic", // ["off", "basic", "strict"]: Specifies the default rule set to use + "typeCheckingMode": "basic", "typeshedPath": "./typings", - // section 3 - set code quality checks - "reportMissingImports": "error", - "reportGeneralTypeIssues": "error", // interesting - "reportUndefinedVariable": "warning", // "_WHO_AM_I_REG" is not defined - Not a showstopper - // section 4 - informational - "reportMissingTypeStubs": "information", - "reportOptionalCall": "information", // Object of type "None" cannot be called - "reportInvalidStringEscapeSequence": "information", - "reportUnboundVariable": "information", - "ReportSelfClsParameterName": "information", - "reportOptionalSubscript": "information", - // section 5 - reduce noise - "reportOptionalMemberAccess": "none", // "read" is not a known member of "None" - occurs often in frozen code - "reportWildcardImportFromLibrary": "none", - "reportUnknownArgumentType": "none", - "reportSelfClsParameterName": "none", - "reportMissingModuleSource": "none", + "reportMissingModuleSource": "none" } \ No newline at end of file diff --git a/tests/quality_tests/check_webassembly/mypy.ini b/tests/quality_tests/check_webassembly/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_webassembly/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_webassembly/pyrightconfig.json b/tests/quality_tests/check_webassembly/pyrightconfig.json new file mode 100644 index 000000000..967c0766e --- /dev/null +++ b/tests/quality_tests/check_webassembly/pyrightconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "." + ], + "typeshedPaths": [ + "./typings" + ], + "exclude": [ + ".*", + "__*", + "**/typings" + ], + "ignore": [ + "**/typings" + ], + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", + "verboseOutput": false, + "typeCheckingMode": "basic", + "typeshedPath": "./typings", + "reportMissingModuleSource": "none" +} \ No newline at end of file diff --git a/tests/quality_tests/check_windows/mypy.ini b/tests/quality_tests/check_windows/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/check_windows/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/check_windows/pyrightconfig.json b/tests/quality_tests/check_windows/pyrightconfig.json new file mode 100644 index 000000000..967c0766e --- /dev/null +++ b/tests/quality_tests/check_windows/pyrightconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "." + ], + "typeshedPaths": [ + "./typings" + ], + "exclude": [ + ".*", + "__*", + "**/typings" + ], + "ignore": [ + "**/typings" + ], + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", + "verboseOutput": false, + "typeCheckingMode": "basic", + "typeshedPath": "./typings", + "reportMissingModuleSource": "none" +} \ No newline at end of file diff --git a/tests/quality_tests/feat_bluetooth/check_examples/ble_bonding_peripheral.py b/tests/quality_tests/feat_bluetooth/check_examples/ble_bonding_peripheral.py index c3ae5f262..28fc6d365 100644 --- a/tests/quality_tests/feat_bluetooth/check_examples/ble_bonding_peripheral.py +++ b/tests/quality_tests/feat_bluetooth/check_examples/ble_bonding_peripheral.py @@ -10,14 +10,14 @@ # includes an implementation of the secret store. See # https://github.com/micropython/micropython-lib/tree/master/micropython/bluetooth/aioble -import bluetooth +import binascii +import json import random import struct import time -import json -import binascii -from ble_advertising import advertising_payload +import bluetooth +from ble_advertising import advertising_payload from micropython import const _IRQ_CENTRAL_CONNECT = const(1) @@ -127,7 +127,8 @@ def _irq(self, event, data): return True elif event == _IRQ_GET_SECRET: sec_type, index, key = data - print("get secret:", sec_type, index, bytes(key) if key else None) + reveal_type(key) # mypy and pylance see this differently; tuple[Any, builtins.bytes] vs Any + print("get secret:", sec_type, index, bytes(key) if key else None) # stubs-ignore : linter=="mypy" if key is None: i = 0 for (t, _key), value in self._secrets.items(): @@ -137,7 +138,7 @@ def _irq(self, event, data): i += 1 return None else: - key = sec_type, bytes(key) + key = sec_type, bytes(key) # stubs-ignore : linter=="mypy" return self._secrets.get(key, None) def set_temperature(self, temp_deg_c, notify=False, indicate=False): diff --git a/tests/quality_tests/feat_bluetooth/mypy.ini b/tests/quality_tests/feat_bluetooth/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_bluetooth/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_bluetooth/pyrightconfig.json b/tests/quality_tests/feat_bluetooth/pyrightconfig.json index 54df19d14..967c0766e 100644 --- a/tests/quality_tests/feat_bluetooth/pyrightconfig.json +++ b/tests/quality_tests/feat_bluetooth/pyrightconfig.json @@ -2,33 +2,21 @@ "include": [ "." ], + "typeshedPaths": [ + "./typings" + ], "exclude": [ ".*", "__*", - "typings" + "**/typings" + ], + "ignore": [ + "**/typings" ], - // section 1 - platform - "pythonVersion": "3.8", - "pythonPlatform": "Linux", // or "All" + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", "verboseOutput": false, - // section 2 - required settings - "typeCheckingMode": "basic", // ["off", "basic", "strict"]: Specifies the default rule set to use + "typeCheckingMode": "basic", "typeshedPath": "./typings", - // section 3 - set code quality checks - "reportMissingImports": "error", - "reportGeneralTypeIssues": "error", // interesting - "reportUndefinedVariable": "warning", // "_WHO_AM_I_REG" is not defined - Not a showstopper - // section 4 - informational - "reportMissingTypeStubs": "information", - "reportOptionalCall": "information", // Object of type "None" cannot be called - "reportInvalidStringEscapeSequence": "information", - "reportUnboundVariable": "information", - "ReportSelfClsParameterName": "information", - "reportOptionalSubscript": "information", - // section 5 - reduce noise - "reportOptionalMemberAccess": "none", // "read" is not a known member of "None" - occurs often in frozen code - "reportWildcardImportFromLibrary": "none", - "reportUnknownArgumentType": "none", - "reportSelfClsParameterName": "none", - "reportMissingModuleSource": "none", + "reportMissingModuleSource": "none" } \ No newline at end of file diff --git a/tests/quality_tests/feat_espnow/check_espnow.py b/tests/quality_tests/feat_espnow/check_espnow.py index 3c73e0d52..380d24c80 100644 --- a/tests/quality_tests/feat_espnow/check_espnow.py +++ b/tests/quality_tests/feat_espnow/check_espnow.py @@ -9,6 +9,7 @@ sta.active(True) sta.disconnect() # For ESP8266 +reveal_type(espnow.ESPNow) e = espnow.ESPNow() e.active(True) peer = b"\xbb\xbb\xbb\xbb\xbb\xbb" # MAC address of peer's wifi interface diff --git a/tests/quality_tests/feat_espnow/check_utils/espnow_scan.py b/tests/quality_tests/feat_espnow/check_utils/espnow_scan.py index 32444db9c..292a069e8 100644 --- a/tests/quality_tests/feat_espnow/check_utils/espnow_scan.py +++ b/tests/quality_tests/feat_espnow/check_utils/espnow_scan.py @@ -39,7 +39,8 @@ def ping_peer(enow, peer, channel, num_pings, verbose): if set_channel(channel) is None: return 0.0 time.sleep(CHANNEL_SETTLING_TIME) - msg = PING_MSG + bytes([channel]) # type: ignore #TODO Operator "+" not supported for types "Literal[b"ping"]" and "bytes" + msg = PING_MSG + bytes([channel]) # type: ignore + #TODO Operator "+" not supported for types "Literal[b"ping"]" and "bytes" frac = sum((enow.send(peer, msg) for _ in range(num_pings))) / num_pings if verbose: print(f"Channel {channel:2d}: ping response rate = {frac * 100:3.0f}%.") diff --git a/tests/quality_tests/feat_espnow/mypy.ini b/tests/quality_tests/feat_espnow/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_espnow/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_espnow/pyrightconfig.json b/tests/quality_tests/feat_espnow/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/feat_espnow/pyrightconfig.json +++ b/tests/quality_tests/feat_espnow/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/feat_machine/mypy.ini b/tests/quality_tests/feat_machine/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_machine/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_machine/pyrightconfig.json b/tests/quality_tests/feat_machine/pyrightconfig.json new file mode 100644 index 000000000..967c0766e --- /dev/null +++ b/tests/quality_tests/feat_machine/pyrightconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "." + ], + "typeshedPaths": [ + "./typings" + ], + "exclude": [ + ".*", + "__*", + "**/typings" + ], + "ignore": [ + "**/typings" + ], + // "pythonVersion": "3.8", + "pythonPlatform": "Linux", + "verboseOutput": false, + "typeCheckingMode": "basic", + "typeshedPath": "./typings", + "reportMissingModuleSource": "none" +} \ No newline at end of file diff --git a/tests/quality_tests/feat_micropython/check_bare_const.py b/tests/quality_tests/feat_micropython/check_bare_const.py index 78121cab2..f2bbca8d4 100644 --- a/tests/quality_tests/feat_micropython/check_bare_const.py +++ b/tests/quality_tests/feat_micropython/check_bare_const.py @@ -1,6 +1,7 @@ """Check const() without importing it from micropython.""" # # ref https://github.dev/microsoft/pyright/blob/3cc4e6ccdde06315f5682d9cf61c51ce6fac2753/docs/builtins.md#L7 -# OK: pyright 1.1.218 can handle this +# pyright 1.1.218+ can handle this +# TODO: mypy: cannot add builtins AFAIK -FOO = const(11) +FOO = const(11) #stubs-ignore : linter=="mypy" # false test outcome : https://github.com/Josverl/micropython-stubber/issues/429 diff --git a/tests/quality_tests/feat_micropython/check_micropython/check_viper.py b/tests/quality_tests/feat_micropython/check_micropython/check_viper.py index 52fb86a76..7681d3f45 100644 --- a/tests/quality_tests/feat_micropython/check_micropython/check_viper.py +++ b/tests/quality_tests/feat_micropython/check_micropython/check_viper.py @@ -7,7 +7,7 @@ @micropython.viper def foo(self, arg: int) -> int: - buf = ptr8(self.linebuf) # self.linebuf is a bytearray or bytes object # type: ignore # + buf = ptr8(self.linebuf) # type: ignore for x in range(20, 30): bar = buf[x] # Access a data item through the pointer # code omitted diff --git a/tests/quality_tests/feat_micropython/mypy.ini b/tests/quality_tests/feat_micropython/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_micropython/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_micropython/pyrightconfig.json b/tests/quality_tests/feat_micropython/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/feat_micropython/pyrightconfig.json +++ b/tests/quality_tests/feat_micropython/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/feat_networking/check_ssl.py b/tests/quality_tests/feat_networking/check_ssl.py index 5e2190644..68f86baa6 100644 --- a/tests/quality_tests/feat_networking/check_ssl.py +++ b/tests/quality_tests/feat_networking/check_ssl.py @@ -45,7 +45,8 @@ def __init__(self, sock, **kwargs): def accept(self): client, addr = self.sock.accept() - return (ssl.wrap_socket(client, cert=cert, key=key, **self.kwargs), # type: ignore : TODO add kwargs to stub definition + # TODO: add kwargs to ssl.wrap_socket stub definition + return (ssl.wrap_socket(client, cert=cert, key=key, **self.kwargs), # type: ignore addr) def close(self): diff --git a/tests/quality_tests/feat_networking/mypy.ini b/tests/quality_tests/feat_networking/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_networking/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_networking/pyrightconfig.json b/tests/quality_tests/feat_networking/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/feat_networking/pyrightconfig.json +++ b/tests/quality_tests/feat_networking/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/feat_stdlib/mypy.ini b/tests/quality_tests/feat_stdlib/mypy.ini index 7d99a01c2..f03131078 100644 --- a/tests/quality_tests/feat_stdlib/mypy.ini +++ b/tests/quality_tests/feat_stdlib/mypy.ini @@ -2,7 +2,7 @@ platform = linux mypy_path = typings files = *.py -exclude = .*typings.*.pyi +exclude = typings[\\/].* custom_typing_module = typings/stdlib/typing.pyi ; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir @@ -10,12 +10,12 @@ follow_imports = silent follow_imports_for_stubs = True no_site_packages = True -check_untyped_defs=True +check_untyped_defs = True ; show_error_context = True disable_error_code = no-redef,assignment -# sippets may re-use the same variable names +# snippets may re-use the same variable names allow_redefinition = True ; warn_return_any = True diff --git a/tests/quality_tests/feat_stdlib/pyrightconfig.json b/tests/quality_tests/feat_stdlib/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/feat_stdlib/pyrightconfig.json +++ b/tests/quality_tests/feat_stdlib/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/feat_stdlib_only/check_os/check_files.py b/tests/quality_tests/feat_stdlib_only/check_os/check_files.py index 15ccc0e50..63dd81b7a 100644 --- a/tests/quality_tests/feat_stdlib_only/check_os/check_files.py +++ b/tests/quality_tests/feat_stdlib_only/check_os/check_files.py @@ -17,7 +17,7 @@ def listdir(path=".", sub=False, JSON=True, gethash=False): # Lists the file information of a folder - li :List[dict]= [] # type: List[dict] + li :List[dict]= [] if path == ".": # Get current folder name path = os.getcwd() files = os.listdir(path) diff --git a/tests/quality_tests/feat_stdlib_only/mypy.ini b/tests/quality_tests/feat_stdlib_only/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_stdlib_only/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_stdlib_only/pyrightconfig.json b/tests/quality_tests/feat_stdlib_only/pyrightconfig.json index 5f32ce248..967c0766e 100644 --- a/tests/quality_tests/feat_stdlib_only/pyrightconfig.json +++ b/tests/quality_tests/feat_stdlib_only/pyrightconfig.json @@ -13,11 +13,10 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", "typeshedPath": "./typings", "reportMissingModuleSource": "none" - } \ No newline at end of file diff --git a/tests/quality_tests/feat_uasyncio/check_demo/aiorepl.py b/tests/quality_tests/feat_uasyncio/check_demo/aiorepl.py index 3e7829d4c..2fdba6af3 100644 --- a/tests/quality_tests/feat_uasyncio/check_demo/aiorepl.py +++ b/tests/quality_tests/feat_uasyncio/check_demo/aiorepl.py @@ -123,7 +123,7 @@ async def task(g=None, prompt="--> "): sys.stdout.write("\n") if cmd: # Push current command. - hist[hist_i] = cmd + hist[hist_i] = cmd # type: ignore # Increase history length if possible, and rotate ring forward. hist_n = min(_HISTORY_LIMIT - 1, hist_n + 1) hist_i = (hist_i + 1) % _HISTORY_LIMIT @@ -162,7 +162,7 @@ async def task(g=None, prompt="--> "): key = await s.read(2) if key in ("[A", "[B"): # Stash the current command. - hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd # type: ignore - irrelevant + hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd # type: ignore # Clear current command. b = "\x08" * len(cmd) # type: ignore sys.stdout.write(b) diff --git a/tests/quality_tests/feat_uasyncio/check_demo/auart_hd.py b/tests/quality_tests/feat_uasyncio/check_demo/auart_hd.py index 0ac285c2c..36c9093f9 100644 --- a/tests/quality_tests/feat_uasyncio/check_demo/auart_hd.py +++ b/tests/quality_tests/feat_uasyncio/check_demo/auart_hd.py @@ -12,9 +12,10 @@ # In this test a physical device is emulated by the Device class # To test link X1-X4 and X2-X3 -from pyb import UART # type: ignore import uasyncio as asyncio -from primitives.delay_ms import Delay_ms # type: ignore +from machine import UART +from primitives.delay_ms import Delay_ms # type: ignore + # Dummy device waits for any incoming line and responds with 4 lines at 1 second # intervals. diff --git a/tests/quality_tests/feat_uasyncio/mypy.ini b/tests/quality_tests/feat_uasyncio/mypy.ini new file mode 100644 index 000000000..a41c589d0 --- /dev/null +++ b/tests/quality_tests/feat_uasyncio/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +platform = linux +mypy_path = typings +files = *.py +exclude = typings[\\/].* +custom_typing_module = typings/stdlib/typing.pyi +; custom_typeshed_dir = typings/stdlib/_typeshed ; not a valid typeshed dir + +follow_imports = silent +follow_imports_for_stubs = True +no_site_packages = True + +check_untyped_defs = True + +; show_error_context = True +disable_error_code = no-redef,assignment + +# snippets may re-use the same variable names +allow_redefinition = True + +; warn_return_any = True +; warn_unused_configs = True diff --git a/tests/quality_tests/feat_uasyncio/pyrightconfig.json b/tests/quality_tests/feat_uasyncio/pyrightconfig.json index e58196fae..967c0766e 100644 --- a/tests/quality_tests/feat_uasyncio/pyrightconfig.json +++ b/tests/quality_tests/feat_uasyncio/pyrightconfig.json @@ -13,7 +13,7 @@ "ignore": [ "**/typings" ], - "pythonVersion": "3.8", + // "pythonVersion": "3.8", "pythonPlatform": "Linux", "verboseOutput": false, "typeCheckingMode": "basic", diff --git a/tests/quality_tests/test_snippets.py b/tests/quality_tests/test_snippets.py index 137202612..15d83c337 100644 --- a/tests/quality_tests/test_snippets.py +++ b/tests/quality_tests/test_snippets.py @@ -1,14 +1,9 @@ -import json import logging -import platform -import re -import subprocess from pathlib import Path -from typing import Dict, List -import fasteners import pytest -from packaging.version import Version +from typecheck import (copy_config_files, port_and_board, run_typechecker, + stub_ignore) # only snippets tests pytestmark = pytest.mark.snippets @@ -57,12 +52,6 @@ ] -def port_and_board(portboard): - if "-" in portboard: - port, board = portboard.split("-", 1) - else: - port, board = portboard, "" - return port, board def pytest_generate_tests(metafunc: pytest.Metafunc): @@ -74,6 +63,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): """ argnames = "stub_source, version, portboard, feature" args_lst = [] + copy_config_files() for src in SOURCES: for version in VERSIONS: # skip latest for pypi @@ -106,130 +96,18 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): metafunc.parametrize(argnames, args_lst, scope="session") -def filter_issues(issues: List[Dict], version: str, portboard: str = ""): - port, board = portboard.split("-") if "-" in portboard else (portboard, "") - for issue in issues: - try: - filename = Path(issue["file"]) - with open(filename, "r") as f: - lines = f.readlines() - line = issue["range"]["start"]["line"] - if len(lines) > line: - theline: str = lines[line] - # check if the line contains a stubs-ignore comment - if stub_ignore(theline, version, port, board): - issue["severity"] = "information" - except KeyError as e: - log.warning(f"Could not process issue: {e} \n{json.dumps(issues, indent=4)}") - return issues - - -def stub_ignore(line, version, port, board, linter="pyright", is_source=True) -> bool: - """ - Check if a typecheck error should be ignored based on the version of micropython , the port and the board - the same syntax can be used in the source file or in the test case condition : - format of the source line (is_source=True) - import espnow # stubs-ignore: version<1.21.0 or not port.startswith('esp') - or condition (is_source=False) line: - version<1.21.0 - skip version<1.21.0 # skip prefix to helps human understanding / reading - skip port.startswith('esp') - """ - if is_source: - comment = line.rsplit("#")[-1].strip() - if not (comment.startswith("stubs-ignore") and ":" in comment): - return False - id, condition = comment.split(":") - if id.strip() != "stubs-ignore": - return False - condition = condition.strip() - else: - condition = line.strip() - if condition.lower().startswith("skip"): - condition = condition[4:].strip() - context = {} - context["Version"] = Version - context["version"] = Version(version) if not version in ("latest", "-") else Version("9999.99.99") - context["port"] = port - context["board"] = board - context["linter"] = linter - - try: - # transform : version>=1.20.1 to version>=Version('1.20.1') using a regular expression - condition = re.sub(r"(\d+\.\d+\.\d+)", r"Version('\1')", condition.strip()) - result = eval(condition, context) - # print(f'stubs-ignore: {condition} -> {"Skip" if result else "Process"}') - except Exception as e: - log.warning(f"Incorrect stubs-ignore condition: `{condition}`\ncaused: {e}") - result = False - - return bool(result) - - -def run_pyright( snip_path, version, portboard, pytestconfig): - """ - Run Pyright static type checker a path with validation code - Args: - snip_path (Path): The path to the project. - version (str): The version of the stubs . - portboard (str): The portboard of the project. - pytestconfig: The pytest configuration object. - - Returns: - tuple: A tuple containing the information message and the number of errors found. - """ - - cmd = f"pyright --project {snip_path.as_posix()} --outputjson" - typecheck_lock = fasteners.InterProcessLock(snip_path / "typecheck_lock.file") - - use_shell = platform.system() != "Windows" - results = {} - with typecheck_lock: - try: - # run pyright in the folder with the check_scripts to allow modules to import each other. - result = subprocess.run(cmd, shell=use_shell, capture_output=True, cwd=snip_path.as_posix()) - except OSError as e: - raise e - if result.returncode >= 2: - assert ( - 0 - ), f"Pyright failed with returncode {result.returncode}: {result.stdout}\n{result.stderr}" - try: - results = json.loads(result.stdout) - except Exception: - assert 0, "Could not load pyright's JSON output..." - - issues: List[Dict] = results["generalDiagnostics"] - # for each of the issues - retrieve the line in the source file to inspect if has a trailing comment - issues = filter_issues(issues, version, portboard) - - # log the errors in the issues list so that pytest will capture the output - for issue in issues: - # log file:line:column?: message - try: - relative = Path(issue["file"]).relative_to(pytestconfig.rootpath).as_posix() - except Exception: - relative = issue["file"] - msg = f"{relative}:{issue['range']['start']['line']+1}:{issue['range']['start']['character']} {issue['message']}" - # caplog.messages.append(msg) - if issue["severity"] == "error": - log.error(msg) - elif issue["severity"] == "warning": - log.warning(msg) - else: - log.info(msg) - - info_msg = f"Pyright found {results['summary']['errorCount']} errors and {results['summary']['warningCount']} warnings in {results['summary']['filesAnalyzed']} files." - errorcount = len([i for i in issues if i["severity"] == "error"]) - return info_msg,errorcount # return issues - -def test_ports_boards_pyright( +@pytest.mark.parametrize( + "linter", + ["pyright", "mypy"], +) +def test_ports_boards_typecheck( + linter:str, stub_source: str, version: str, portboard: str, @@ -243,7 +121,7 @@ def test_ports_boards_pyright( FileNotFoundError(f"no feature folder for {feature}") caplog.set_level(logging.INFO) - log.info(f"PYRIGHT {portboard}, {feature} {version} from {stub_source}") + log.info(f"Typecheck {linter} on {portboard}, {feature} {version} from {stub_source}") - info_msg, errorcount = run_pyright(snip_path, version, portboard, pytestconfig) + info_msg, errorcount = run_typechecker(snip_path, version, portboard, pytestconfig, linter=linter) assert errorcount == 0, info_msg diff --git a/tests/quality_tests/test_stdlib.py b/tests/quality_tests/test_stdlib.py index b1a19549e..4cb1c38d0 100644 --- a/tests/quality_tests/test_stdlib.py +++ b/tests/quality_tests/test_stdlib.py @@ -3,8 +3,7 @@ from typing import Dict, List import pytest - -from test_snippets import SOURCES, run_pyright +from test_snippets import SOURCES, run_typechecker # only snippets tests pytestmark = pytest.mark.snippets @@ -18,13 +17,18 @@ @pytest.mark.parametrize("feature", ["stdlib"],scope="session") @pytest.mark.parametrize("stub_source", SOURCES,scope="session") @pytest.mark.parametrize("snip_path", [HERE / "feat_stdlib_only"],scope="session") -def test_stdlib_pyright( +@pytest.mark.parametrize( + "linter", + ["pyright", "mypy"], +) +def test_stdlib_typecheck( type_stub_cache_path: Path, stub_source: str, portboard: str, feature: str, snip_path: Path, version: str, + linter:str, copy_type_stubs, # Avoid needing autouse fixture caplog: pytest.LogCaptureFixture, pytestconfig: pytest.Config, @@ -33,7 +37,7 @@ def test_stdlib_pyright( if not snip_path or not snip_path.exists(): FileNotFoundError(f"no feature folder for {feature}") caplog.set_level(logging.INFO) - log.info(f"PYRIGHT {portboard}, {feature} from {stub_source}") + log.info(f"Typechecker {linter} : {portboard}, {feature} from {stub_source}") - info_msg, errorcount = run_pyright(snip_path, version, portboard, pytestconfig) + info_msg, errorcount = run_typechecker(snip_path, version, portboard, pytestconfig, linter=linter) assert errorcount == 0, info_msg \ No newline at end of file diff --git a/tests/quality_tests/typecheck.py b/tests/quality_tests/typecheck.py new file mode 100644 index 000000000..17c78e438 --- /dev/null +++ b/tests/quality_tests/typecheck.py @@ -0,0 +1,293 @@ +import json +import logging +import platform +import re +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Dict, List + +import fasteners +import pytest +from mypy_gitlab_code_quality import generate_report as gitlab_report +from packaging.version import Version + +log = logging.getLogger() + + +def copy_config_files(): + # copy the config files from the __config folder to all check_* and feat_* folders + # so that the typecheck can be run from the command line + my_path = Path(__file__).parent.absolute() + config_path = my_path / "_configs" + for folder in list(my_path.glob("check_*")) + list(my_path.glob("feat_*")): + for file in config_path.glob("*.*"): + if file.name == "readme.md": + continue + try: + shutil.copy(file, folder) + except Exception as e: + print(f"Could not copy {file} to {folder} : {e}") + pass + + +def port_and_board(portboard): + if "-" in portboard: + port, board = portboard.split("-", 1) + else: + port, board = portboard, "" + return port, board + +def stub_ignore(line, version, port, board, linter, is_source=True) -> bool: + """ + Check if a typecheck error should be ignored based on the version of micropython , the port and the board + the same syntax can be used in the source file or in the test case condition : + + format of the source line (is_source=True) + import espnow # stubs-ignore: version<1.21.0 or not port.startswith('esp') + + or condition (is_source=False) line: + version<1.21.0 + skip version<1.21.0 # skip prefix to helps human understanding / reading + skip port.startswith('esp') + """ + if is_source: + comment = line.rsplit("#")[-1].strip() + if not (comment.startswith("stubs-ignore") and ":" in comment): + return False + id, condition = comment.split(":") + if id.strip() != "stubs-ignore": + return False + condition = condition.strip() + else: + condition = line.strip() + if condition.lower().startswith("skip"): + condition = condition[4:].strip() + context = {} + context["Version"] = Version + context["version"] = Version(version) if not version in ("latest", "-") else Version("9999.99.99") + context["port"] = port + context["board"] = board + context["linter"] = linter + + try: + # transform : version>=1.20.1 to version>=Version('1.20.1') using a regular expression + condition = re.sub(r"(\d+\.\d+\.\d+)", r"Version('\1')", condition.strip()) + result = eval(condition, context) + # print(f'stubs-ignore: {condition} -> {"Skip" if result else "Process"}') + except Exception as e: + log.warning(f"Incorrect stubs-ignore condition: `{condition}`\ncaused: {e}") + result = False + + return bool(result) + + +def filter_issues(issues: List[Dict], version: str,*, linter:str, portboard: str = ""): + port, board = portboard.split("-") if "-" in portboard else (portboard, "") + for issue in issues: + try: + filename = Path(issue["file"]) + with open(filename, "r") as f: + lines = f.readlines() + line = issue["range"]["start"]["line"] + if len(lines) > line: + theline: str = lines[line] + # check if the line contains a stubs-ignore comment + if stub_ignore(theline, version, port, board, linter=linter): + issue["severity"] = "information" + except KeyError as e: + log.warning(f"Could not process issue: {e} \n{json.dumps(issues, indent=4)}") + return issues + +def run_typechecker( snip_path:Path, version:str, portboard:str, pytestconfig:pytest.Config, *, linter:str,): + """ + Run Pyright static type checker a path with validation code + + Args: + snip_path (Path): The path to the project. + version (str): The version of the stubs . + portboard (str): The portboard of the project. + pytestconfig: The pytest configuration object. + + Returns: + tuple: A tuple containing the information message and the number of errors found. + """ + + typecheck_lock = fasteners.InterProcessLock(snip_path / "typecheck_lock.file") + + results = {} + with typecheck_lock: + if linter=="pyright": + results = check_with_pyright(snip_path) + elif linter=="mypy": + results = check_with_mypy(snip_path) + else: + raise NotImplementedError(f"Unknown linter {linter}") + results = [] + if not results or not "generalDiagnostics" in results: + pytest.xfail(f"Could not run {linter} on {snip_path}") + + issues: List[Dict] = results["generalDiagnostics"] + # for each of the issues - retrieve the line in the source file to inspect if has a trailing comment + issues = filter_issues(issues, version, portboard=portboard, linter=linter) + + # log the errors in the issues list so that pytest will capture the output + for issue in issues: + # log file:line:column?: message + try: + relative = Path(issue["file"]).relative_to(pytestconfig.rootpath).as_posix() + except Exception: + relative = issue["file"] + msg = f"{relative}:{issue['range']['start']['line']+1}:{issue['range']['start']['character']} {issue['message']}" + # caplog.messages.append(msg) + if issue["severity"] == "error": + log.error(msg) + elif issue["severity"] == "warning": + log.warning(msg) + else: + log.info(msg) + + info_msg = f"{linter} found {results['summary']['errorCount']} errors and {results['summary']['warningCount']} warnings in {results['summary']['filesAnalyzed']} files." + errorcount = len([i for i in issues if i["severity"] == "error"]) + return info_msg,errorcount + + +#===================================================================================== + +def check_with_pyright(snip_path): + + cmd = f"pyright --project {snip_path.as_posix()} --outputjson" + use_shell = platform.system() != "Windows" + results = {} + try: + # run pyright in the folder with the check_scripts to allow modules to import each other. + result = subprocess.run(cmd, shell=use_shell, capture_output=True, cwd=snip_path.as_posix()) + except OSError as e: + raise e + if result.returncode >= 2: + assert ( + 0 + ), f"Pyright failed with returncode {result.returncode}: {result.stdout}\n{result.stderr}" + try: + results = json.loads(result.stdout) + except Exception: + assert 0, "Could not load pyright's JSON output..." + return results + +#===================================================================================== +def check_with_mypy(snip_path): + """ + Run mypy on the specified path and return the type checking results. + + Args: + snip_path (str): The path to the code snippet to be checked. + + Returns: + json: The type checking results in pyright format. + + """ + raw_results = run_mypy(snip_path) + results = raw_results.split("\n") + results = gitlab_to_pyright(gitlab_report(map(str.rstrip, results))) + return results + + +def mypy_patch(check_path): + if not check_path.exists(): + Exception(f"Path {check_path} not found") + + for f in ("typings/sys.pyi",): + file = check_path / f + + if file.exists(): + print(f"Removing {f}") + if file.is_file(): + file.unlink() + + elif file.is_dir(): + shutil.rmtree(file) + + +def run_mypy(path: Path): + print(f"Running mypy in {path}") + mypy_patch(path) + cmd = [ sys.executable, "-m", "mypy", "--no-error-summary", "--no-color","--show-absolute-path", "."] + try: + output = subprocess.run( + cmd, + # check=True, + cwd=path, + capture_output=True, + text=True, + shell=False, + ) + return output.stdout + except subprocess.CalledProcessError as e: + print(e) + +# convert from gitlab to pyright format + +HEADER = """ +{ + "version": "", + "time": "", + "generalDiagnostics": [], + "summary": { + "filesAnalyzed": 0, + "errorCount": 0, + "warningCount": 0, + "informationCount": 0, + "timeInSec": 0 + } +} +""" +DIAGNOSTIC = """ +{ + "file": "", + "severity": "", + "message": "", + "rule": "", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 999, + "character": 99 + } + } +} +""" + +severity_map = { + "major": "error", + "warning": "warning", + "info": "information", + "unknown": "information", +} +def gitlab_to_pyright(report): + pyright_report = json.loads(HEADER) + pyright_report["version"] = "1.7.0" # todo: get version from mypy + pyright_report["generalDiagnostics"] = [] + + for issue in report: + i = json.loads(DIAGNOSTIC) + i["file"] = issue["location"]["path"] + i["severity"] = severity_map[issue["severity"]] + i["message"] = issue["description"] + i["rule"] = issue["check_name"] + # pyright uses 0-based lines - gitlab uses 1-based lines + i["range"]["start"]["line"] = int(issue["location"]["lines"]["begin"]) -1 + pyright_report["generalDiagnostics"].append(i) + + for sev in ["error", "warning", "information"]: + count = len([d for d in pyright_report["generalDiagnostics"] if d["severity"] == sev]) + pyright_report["summary"][f"{sev}Count"] = count + + + return pyright_report + + + diff --git a/tests/test_101.py b/tests/test_101.py deleted file mode 100644 index 79711f75c..000000000 --- a/tests/test_101.py +++ /dev/null @@ -1,8 +0,0 @@ -########################## -import pytest - -########################## - - -def test_dummy(): - assert True