diff --git a/extensions/positron-python/.vscodeignore b/extensions/positron-python/.vscodeignore index e4b27f232939..6788f9b6d8e1 100644 --- a/extensions/positron-python/.vscodeignore +++ b/extensions/positron-python/.vscodeignore @@ -57,7 +57,6 @@ out/test/** out/testMultiRootWkspc/** precommit.hook pythonFiles/**/*.pyc -pythonFiles/lib/**/*.dist-info/** pythonFiles/lib/**/*.egg-info/** pythonFiles/lib/python/bin/** pythonFiles/jedilsp_requirements/** diff --git a/extensions/positron-python/build/test_update_ext_version.py b/extensions/positron-python/build/test_update_ext_version.py index 1a2fdb0ecb85..b94484775f59 100644 --- a/extensions/positron-python/build/test_update_ext_version.py +++ b/extensions/positron-python/build/test_update_ext_version.py @@ -1,13 +1,16 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import datetime import json import freezegun import pytest import update_ext_version -TEST_DATETIME = "2022-03-14 01:23:45" + +CURRENT_YEAR = datetime.datetime.now().year +TEST_DATETIME = f"{CURRENT_YEAR}-03-14 01:23:45" # The build ID is calculated via: # "1" + datetime.datetime.strptime(TEST_DATETIME,"%Y-%m-%d %H:%M:%S").strftime('%j%H%M') @@ -31,14 +34,21 @@ def run_test(tmp_path, version, args, expected): @pytest.mark.parametrize( "version, args", [ - ("1.0.0-rc", []), - ("1.1.0-rc", ["--release"]), - ("1.0.0-rc", ["--release", "--build-id", "-1"]), - ("1.0.0-rc", ["--release", "--for-publishing", "--build-id", "-1"]), - ("1.0.0-rc", ["--release", "--for-publishing", "--build-id", "999999999999"]), - ("1.1.0-rc", ["--build-id", "-1"]), - ("1.1.0-rc", ["--for-publishing", "--build-id", "-1"]), - ("1.1.0-rc", ["--for-publishing", "--build-id", "999999999999"]), + ("2000.1.0", []), # Wrong year for CalVer + (f"{CURRENT_YEAR}.0.0-rc", []), + (f"{CURRENT_YEAR}.1.0-rc", ["--release"]), + (f"{CURRENT_YEAR}.0.0-rc", ["--release", "--build-id", "-1"]), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--for-publishing", "--build-id", "-1"], + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--for-publishing", "--build-id", "999999999999"], + ), + (f"{CURRENT_YEAR}.1.0-rc", ["--build-id", "-1"]), + (f"{CURRENT_YEAR}.1.0-rc", ["--for-publishing", "--build-id", "-1"]), + (f"{CURRENT_YEAR}.1.0-rc", ["--for-publishing", "--build-id", "999999999999"]), ], ) def test_invalid_args(tmp_path, version, args): @@ -49,56 +59,68 @@ def test_invalid_args(tmp_path, version, args): @pytest.mark.parametrize( "version, args, expected", [ - ("1.1.0-rc", ["--build-id", "12345"], ("1", "1", "12345", "rc")), - ("1.0.0-rc", ["--release", "--build-id", "12345"], ("1", "0", "12345", "")), ( - "1.1.0-rc", + f"{CURRENT_YEAR}.1.0-rc", + ["--build-id", "12345"], + (f"{CURRENT_YEAR}", "1", "12345", "rc"), + ), + ( + f"{CURRENT_YEAR}.0.0-rc", + ["--release", "--build-id", "12345"], + (f"{CURRENT_YEAR}", "0", "12345", ""), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", ["--for-publishing", "--build-id", "12345"], - ("1", "1", "12345", ""), + (f"{CURRENT_YEAR}", "1", "12345", ""), ), ( - "1.0.0-rc", + f"{CURRENT_YEAR}.0.0-rc", ["--release", "--for-publishing", "--build-id", "12345"], - ("1", "0", "12345", ""), + (f"{CURRENT_YEAR}", "0", "12345", ""), ), ( - "1.0.0-rc", + f"{CURRENT_YEAR}.0.0-rc", ["--release", "--build-id", "999999999999"], - ("1", "0", "999999999999", ""), + (f"{CURRENT_YEAR}", "0", "999999999999", ""), ), ( - "1.1.0-rc", + f"{CURRENT_YEAR}.1.0-rc", ["--build-id", "999999999999"], - ("1", "1", "999999999999", "rc"), + (f"{CURRENT_YEAR}", "1", "999999999999", "rc"), + ), + ( + f"{CURRENT_YEAR}.1.0-rc", + [], + (f"{CURRENT_YEAR}", "1", EXPECTED_BUILD_ID, "rc"), ), - ("1.1.0-rc", [], ("1", "1", EXPECTED_BUILD_ID, "rc")), ( - "1.0.0-rc", + f"{CURRENT_YEAR}.0.0-rc", ["--release"], - ("1", "0", "0", ""), + (f"{CURRENT_YEAR}", "0", "0", ""), ), ( - "1.1.0-rc", + f"{CURRENT_YEAR}.1.0-rc", ["--for-publishing"], - ("1", "1", EXPECTED_BUILD_ID, ""), + (f"{CURRENT_YEAR}", "1", EXPECTED_BUILD_ID, ""), ), ( - "1.0.0-rc", + f"{CURRENT_YEAR}.0.0-rc", ["--release", "--for-publishing"], - ("1", "0", "0", ""), + (f"{CURRENT_YEAR}", "0", "0", ""), ), ( - "1.0.0-rc", + f"{CURRENT_YEAR}.0.0-rc", ["--release"], - ("1", "0", "0", ""), + (f"{CURRENT_YEAR}", "0", "0", ""), ), ( - "1.1.0-rc", + f"{CURRENT_YEAR}.1.0-rc", [], - ("1", "1", EXPECTED_BUILD_ID, "rc"), + (f"{CURRENT_YEAR}", "1", EXPECTED_BUILD_ID, "rc"), ), ], ) -@freezegun.freeze_time("2022-03-14 01:23:45") +@freezegun.freeze_time(f"{CURRENT_YEAR}-03-14 01:23:45") def test_update_ext_version(tmp_path, version, args, expected): run_test(tmp_path, version, args, expected) diff --git a/extensions/positron-python/build/update_ext_version.py b/extensions/positron-python/build/update_ext_version.py index 7a174d42668f..bfd7ac1e9996 100644 --- a/extensions/positron-python/build/update_ext_version.py +++ b/extensions/positron-python/build/update_ext_version.py @@ -69,6 +69,13 @@ def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: major, minor, micro, suffix = parse_version(package["version"]) + current_year = datetime.datetime.now().year + if int(major) != current_year: + raise ValueError( + f"Major version [{major}] must be the current year [{current_year}].", + f"If changing major version after new year's, change to {current_year}.1.0", + ) + if args.release and not is_even(minor): raise ValueError( f"Release version should have EVEN numbered minor version: {package['version']}" diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 63432ececb85..b3c2bd63d640 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -398,7 +398,7 @@ }, "python.defaultInterpreterPath": { "default": "python", - "description": "%python.defaultInterpreterPath.description%", + "markdownDescription": "%python.defaultInterpreterPath.description%", "scope": "machine-overridable", "type": "string" }, @@ -422,7 +422,7 @@ }, "python.experiments.optInto": { "default": [], - "description": "%python.experiments.optInto.description%", + "markdownDescription": "%python.experiments.optInto.description%", "items": { "enum": [ "All", @@ -436,7 +436,7 @@ }, "python.experiments.optOutFrom": { "default": [], - "description": "%python.experiments.optOutFrom.description%", + "markdownDescription": "%python.experiments.optOutFrom.description%", "items": { "enum": [ "All", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index 0c2621b480f6..cc1926b6f91e 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -29,12 +29,12 @@ "python.menu.createNewFile.title": "Python File", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", - "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See https://aka.ms/AAfekmf to understand when this is used", + "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", "python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.", "python.envFile.description": "Absolute path to a file containing environment variable definitions.", "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", - "python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/AB-Experiments for more details.", - "python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/AB-Experiments for more details.", + "python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", + "python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", @@ -115,6 +115,6 @@ "python.testing.unittestEnabled.description": "Enable testing using unittest.", "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", - "python.sortImports.args.deprecationMessage": "This setting will be removed soon. Use `isort.args` instead.", - "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use `isort.path` instead." + "python.sortImports.args.deprecationMessage": "This setting will be removed soon. Use 'isort.args' instead.", + "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead." } diff --git a/extensions/positron-python/pythonFiles/install_debugpy.py b/extensions/positron-python/pythonFiles/install_debugpy.py index 12a9e47cc396..b7f663c01907 100644 --- a/extensions/positron-python/pythonFiles/install_debugpy.py +++ b/extensions/positron-python/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.5" # can also be "latest" +DEBUGGER_VERSION = "1.6.6" # can also be "latest" def _contains(s, parts=()): diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt index 0ab00132f463..062b037e0783 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt +++ b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt @@ -1,72 +1,90 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # pip-compile --generate-hashes 'pythonFiles\jedilsp_requirements\requirements.in' # -docstring-to-markdown==0.10 \ - --hash=sha256:12f75b0c7b7572defea2d9e24b57ef7ac38c3e26e91c0e5547cfc02b1c168bf6 \ - --hash=sha256:a2cd520599d1499d4a5d4eb16dea5bdebe32e5627504fb417d5733570f3d4d0b +attrs==22.2.0 \ + --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ + --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 + # via + # cattrs + # lsprotocol +cattrs==22.2.0 \ + --hash=sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21 \ + --hash=sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d + # via lsprotocol +docstring-to-markdown==0.11 \ + --hash=sha256:01900aee1bc7fde5aacaf319e517a5e1d4f0bf04e401373c08d28fcf79bfb73b \ + --hash=sha256:5b1da2c89d9d0d09b955dec0ee111284ceadd302a938a03ed93f66e09134f9b5 # via jedi-language-server +exceptiongroup==1.1.0 \ + --hash=sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e \ + --hash=sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23 + # via cattrs importlib-metadata==3.10.1 \ --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 # via jedi-language-server -jedi==0.18.1 \ - --hash=sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d \ - --hash=sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab +jedi==0.18.2 \ + --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ + --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 # via jedi-language-server -jedi-language-server==0.38.0 \ - --hash=sha256:573f790267cd149fe356d974da5e9be5f219dea9f5659895487122f7fad120c0 \ - --hash=sha256:98f439926da1ce0410d6f52d0bc0ffbefdfe71fbbd2c7d009ef9e162175c2548 +jedi-language-server==0.40.0 \ + --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ + --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 # via -r pythonFiles\jedilsp_requirements\requirements.in +lsprotocol==2022.0.0a10 \ + --hash=sha256:2cd78770b7a4ec979f3ee3761265effd50ea0f5e858ce21bf2fba972e1783c50 \ + --hash=sha256:ef516aec43c2b3c8debc06e84558ea9a64c36d635422d1614fd7fd2a45b1d291 + # via + # jedi-language-server + # pygls parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.2 \ - --hash=sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42 \ - --hash=sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624 \ - --hash=sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e \ - --hash=sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559 \ - --hash=sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709 \ - --hash=sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9 \ - --hash=sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d \ - --hash=sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52 \ - --hash=sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda \ - --hash=sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912 \ - --hash=sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c \ - --hash=sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525 \ - --hash=sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe \ - --hash=sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41 \ - --hash=sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b \ - --hash=sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283 \ - --hash=sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965 \ - --hash=sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c \ - --hash=sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410 \ - --hash=sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5 \ - --hash=sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116 \ - --hash=sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98 \ - --hash=sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f \ - --hash=sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644 \ - --hash=sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13 \ - --hash=sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd \ - --hash=sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254 \ - --hash=sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6 \ - --hash=sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488 \ - --hash=sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5 \ - --hash=sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c \ - --hash=sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1 \ - --hash=sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a \ - --hash=sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2 \ - --hash=sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d \ - --hash=sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236 - # via - # jedi-language-server - # pygls -pygls==0.12.4 \ - --hash=sha256:1b96378452217a02f19d89d9e647a4256d8d445ab3c641a589b4f73bf11898b6 \ - --hash=sha256:63b859411307ed6f99fb9dd0e71be507a17ae9b3de5c5d07c497f5bddadcc46a +pydantic==1.10.4 \ + --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ + --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ + --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ + --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ + --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ + --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ + --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ + --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ + --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ + --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ + --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ + --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ + --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ + --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ + --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ + --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ + --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ + --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ + --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ + --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ + --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ + --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ + --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ + --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ + --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ + --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ + --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ + --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ + --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ + --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ + --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ + --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ + --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ + --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ + --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ + --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d + # via jedi-language-server +pygls==1.0.0 \ + --hash=sha256:3414594ac29ff3ab990f004c675d1077e4e2659eae5cc3ae67cc6fa4d861e342 \ + --hash=sha256:c2a1c22e30028f7ca9d3f0a04da8eef29f0f1701bdbd97d8614d8e1e6711f336 # via # -r pythonFiles\jedilsp_requirements\requirements.in # jedi-language-server @@ -78,9 +96,10 @@ typing-extensions==4.4.0 \ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via + # cattrs # importlib-metadata # pydantic -zipp==3.10.0 \ - --hash=sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1 \ - --hash=sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8 +zipp==3.12.0 \ + --hash=sha256:73efd63936398aac78fd92b6f4865190119d6c91b531532e798977ea8dd402eb \ + --hash=sha256:9eb0a4c5feab9b08871db0d672745b53450d7f26992fd1e4653aa43345e97b86 # via importlib-metadata diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/info.py b/extensions/positron-python/pythonFiles/testing_tools/adapter/info.py index f99ce0b6f9a2..d518a29dd97a 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/info.py +++ b/extensions/positron-python/pythonFiles/testing_tools/adapter/info.py @@ -27,7 +27,6 @@ def __init__(self, *args, **kwargs): class ParentInfo(namedtuple("ParentInfo", "id kind name root relpath parentid")): - KINDS = ("folder", "file", "suite", "function", "subtest") def __new__(cls, id, kind, name, root=None, relpath=None, parentid=None): diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py index 34312dcc1997..4b852ecf81c9 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ b/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py @@ -7,7 +7,7 @@ import pytest -from .. import util, discovery +from .. import discovery, util from ._pytest_item import parse_item @@ -26,7 +26,7 @@ def discover( pytestargs = _adjust_pytest_args(pytestargs) # We use this helper rather than "-pno:terminal" due to possible # platform-dependent issues. - with (util.hide_stdio() if hidestdio else util.noop_cm()) as stdio: + with util.hide_stdio() if hidestdio else util.noop_cm() as stdio: ec = _pytest_main(pytestargs, [_plugin]) # See: https://docs.pytest.org/en/latest/usage.html#possible-exit-codes if ec == 5: diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py index c3b8a2d679d1..83eeaa1f9062 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py @@ -46,7 +46,6 @@ def main(self, args, plugins): class StubPlugin(util.StubProxy): - _started = True def __init__(self, stub=None, tests=None): @@ -66,7 +65,6 @@ def func(*args, **kwargs): class StubDiscoveredTests(util.StubProxy): - NOT_FOUND = object() def __init__(self, stub=None): @@ -105,7 +103,6 @@ def __init__(self, name): class StubPytestItem(util.StubProxy): - _debugging = False _hasfunc = True @@ -218,6 +215,7 @@ def normcase(path): else: raise NotImplementedError + ########## def _fix_fileid(*args): return adapter_util.fix_fileid( @@ -332,7 +330,6 @@ def ret(args, plugins): class DiscoverTests(unittest.TestCase): - DEFAULT_ARGS = [ "--collect-only", ] diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test___main__.py b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test___main__.py index d0a778c1d024..5ff0ec30c947 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test___main__.py +++ b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test___main__.py @@ -3,14 +3,15 @@ import unittest -from ...util import Stub, StubProxy from testing_tools.adapter.__main__ import ( - parse_args, - main, - UnsupportedToolError, UnsupportedCommandError, + UnsupportedToolError, + main, + parse_args, ) +from ...util import Stub, StubProxy + class StubTool(StubProxy): def __init__(self, name, stub=None): @@ -115,7 +116,6 @@ def test_unsupported_tool(self): class MainTests(unittest.TestCase): - # TODO: We could use an integration test for pytest.discover(). def test_discover(self): diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py index ec3d198b0108..cf3b8fb3139b 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py @@ -5,13 +5,12 @@ import unittest +from testing_tools.adapter.discovery import DiscoveredTests, fix_nodeid +from testing_tools.adapter.info import ParentInfo, SingleTestInfo, SingleTestPath from testing_tools.adapter.util import fix_path, fix_relpath -from testing_tools.adapter.info import SingleTestInfo, SingleTestPath, ParentInfo -from testing_tools.adapter.discovery import fix_nodeid, DiscoveredTests def _fix_nodeid(nodeid): - nodeid = nodeid.replace("\\", "/") if not nodeid.startswith("./"): nodeid = "./" + nodeid diff --git a/extensions/positron-python/pythonFiles/unittestadapter/execution.py b/extensions/positron-python/pythonFiles/unittestadapter/execution.py index a016ff1af9ec..a926bcdcc09e 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/execution.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/execution.py @@ -60,7 +60,6 @@ class TestOutcomeEnum(str, enum.Enum): class UnittestTestResult(unittest.TextTestResult): - formatted: Dict[str, Dict[str, str | None]] = dict() def startTest(self, test: unittest.TestCase): diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts b/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts index 45a758c9f283..4f5133d9dcbe 100644 --- a/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -34,7 +34,7 @@ const messages = { 'No Python interpreter is selected. Please select a Python interpreter to enable features such as IntelliSense, linting, and debugging.', ), [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: l10n.t( - 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging.', + 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging. See output for more details regarding why the interpreter is invalid.', ), }; @@ -163,7 +163,7 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - return [ + const prompts = [ { prompt: Common.selectPythonInterpreter, command: commandFactory.createCommand(diagnostic, { @@ -172,6 +172,16 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService }), }, ]; + if (diagnostic.code === DiagnosticCodes.InvalidPythonInterpreterDiagnostic) { + prompts.push({ + prompt: Common.openOutputPanel, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: Commands.ViewOutput, + }), + }); + } + return prompts; } } diff --git a/extensions/positron-python/src/client/common/extensions.ts b/extensions/positron-python/src/client/common/extensions.ts index 962349583776..e68e3838ee1d 100644 --- a/extensions/positron-python/src/client/common/extensions.ts +++ b/extensions/positron-python/src/client/common/extensions.ts @@ -1,21 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/** - * @typedef {Object} SplitLinesOptions - * @property {boolean} [trim=true] - Whether to trim the lines. - * @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. - */ - -// https://stackoverflow.com/questions/39877156/how-to-extend-string-prototype-and-use-it-next-in-typescript - declare interface String { - /** - * Split a string using the cr and lf characters and return them as an array. - * By default lines are trimmed and empty lines are removed. - * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. - */ - splitLines(splitOptions?: { trim: boolean; removeEmptyEntries?: boolean }): string[]; /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. @@ -37,33 +23,8 @@ declare interface String { * Removes leading and trailing quotes from a string */ trimQuotes(): string; - - /** - * String.replaceAll implementation - * Replaces all instances of a substring with a new string - */ - replaceAll(substr: string, newSubstr: string): string; } -/** - * Split a string using the cr and lf characters and return them as an array. - * By default lines are trimmed and empty lines are removed. - * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. - */ -String.prototype.splitLines = function ( - this: string, - splitOptions: { trim: boolean; removeEmptyEntries: boolean } = { removeEmptyEntries: true, trim: true }, -): string[] { - let lines = this.split(/\r?\n/g); - if (splitOptions && splitOptions.trim) { - lines = lines.map((line) => line.trim()); - } - if (splitOptions && splitOptions.removeEmptyEntries) { - lines = lines.filter((line) => line.length > 0); - } - return lines; -}; - /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. @@ -102,26 +63,6 @@ String.prototype.trimQuotes = function (this: string): string { return this.replace(/(^['"])|(['"]$)/g, ''); }; -/** - * String.replaceAll implementation - * Replaces all instances of a substring with a new substring. - */ -String.prototype.replaceAll = function (this: string, substr: string, newSubstr: string): string { - if (!this) { - return this; - } - - /** Escaping function from the MDN web docs site - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - * Escapes all the following special characters in a string . * + ? ^ $ { } ( ) | \ \\ */ - - function escapeRegExp(unescapedStr: string): string { - return unescapedStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - } - - return this.replace(new RegExp(escapeRegExp(substr), 'g'), newSubstr); -}; - declare interface Promise { /** * Catches task error and ignores them. diff --git a/extensions/positron-python/src/client/common/installer/condaInstaller.ts b/extensions/positron-python/src/client/common/installer/condaInstaller.ts index 774cade34457..a20b35e0f110 100644 --- a/extensions/positron-python/src/client/common/installer/condaInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/condaInstaller.ts @@ -5,6 +5,7 @@ import { inject, injectable } from 'inversify'; import { ICondaService, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { getEnvPath } from '../../pythonEnvironments/base/info/env'; import { ModuleInstallerType } from '../../pythonEnvironments/info'; import { ExecutionInfo, IConfigurationService, Product } from '../types'; import { isResource } from '../utils/misc'; @@ -79,7 +80,7 @@ export class CondaInstaller extends ModuleInstaller { const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath - : resource.id ?? ''; + : getEnvPath(resource.path, resource.envPath).path ?? ''; const condaLocatorService = this.serviceContainer.get(IComponentAdapter); const info = await condaLocatorService.getCondaEnvironment(pythonPath); const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install']; @@ -132,7 +133,7 @@ export class CondaInstaller extends ModuleInstaller { const condaService = this.serviceContainer.get(IComponentAdapter); const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath - : resource.id ?? ''; + : getEnvPath(resource.path, resource.envPath).path ?? ''; return condaService.isCondaEnvironment(pythonPath); } } diff --git a/extensions/positron-python/src/client/common/process/logger.ts b/extensions/positron-python/src/client/common/process/logger.ts index 6a8e25d76e0b..ebb1ad019a48 100644 --- a/extensions/positron-python/src/client/common/process/logger.ts +++ b/extensions/positron-python/src/client/common/process/logger.ts @@ -11,6 +11,7 @@ import { Logging } from '../utils/localize'; import { getOSType, getUserHomeDir, OSType } from '../utils/platform'; import { IProcessLogger, SpawnOptions } from './types'; import { escapeRegExp } from 'lodash'; +import { replaceAll } from '../stringUtils'; @injectable() export class ProcessLogger implements IProcessLogger { @@ -57,7 +58,7 @@ function replaceMatchesWithCharacter(original: string, match: string, character: let pattern = escapeRegExp(match); if (getOSType() === OSType.Windows) { // Match both forward and backward slash versions of 'match' for Windows. - pattern = pattern.replaceAll('\\\\', '(\\\\|/)'); + pattern = replaceAll(pattern, '\\\\', '(\\\\|/)'); } let regex = new RegExp(pattern, 'ig'); return regex; diff --git a/extensions/positron-python/src/client/common/stringUtils.ts b/extensions/positron-python/src/client/common/stringUtils.ts new file mode 100644 index 000000000000..02ca51082ea8 --- /dev/null +++ b/extensions/positron-python/src/client/common/stringUtils.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface SplitLinesOptions { + trim?: boolean; + removeEmptyEntries?: boolean; +} + +/** + * Split a string using the cr and lf characters and return them as an array. + * By default lines are trimmed and empty lines are removed. + * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. + */ +export function splitLines( + source: string, + splitOptions: SplitLinesOptions = { removeEmptyEntries: true, trim: true }, +): string[] { + let lines = source.split(/\r?\n/g); + if (splitOptions?.trim) { + lines = lines.map((line) => line.trim()); + } + if (splitOptions?.removeEmptyEntries) { + lines = lines.filter((line) => line.length > 0); + } + return lines; +} + +/** + * Replaces all instances of a substring with a new substring. + */ +export function replaceAll(source: string, substr: string, newSubstr: string): string { + if (!source) { + return source; + } + + /** Escaping function from the MDN web docs site + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + * Escapes all the following special characters in a string . * + ? ^ $ { } ( ) | \ \\ + */ + + function escapeRegExp(unescapedStr: string): string { + return unescapedStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } + + return source.replace(new RegExp(escapeRegExp(substr), 'g'), newSubstr); +} diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index 9680a30a460b..6c8dbcf06663 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -48,6 +48,8 @@ export namespace Diagnostics { } export namespace Common { + export const allow = l10n.t('Allow'); + export const close = l10n.t('Close'); export const bannerLabelYes = l10n.t('Yes'); export const bannerLabelNo = l10n.t('No'); export const yesPlease = l10n.t('Yes, please'); @@ -189,7 +191,7 @@ export namespace Interpreters { export const discovering = l10n.t('Discovering Python Interpreters'); export const refreshing = l10n.t('Refreshing Python Interpreters'); export const condaInheritEnvMessage = l10n.t( - 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings.', + 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. [Learn more](https://aka.ms/AA66i8f).', ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index d13cf48c2a6d..2d80f0e3d6e8 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts @@ -10,6 +10,7 @@ import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; import { IDynamicDebugConfigurationService } from '../types'; import { DebuggerTypeName } from '../../constants'; import { asyncFilter } from '../../../common/utils/arrayUtils'; +import { replaceAll } from '../../../common/stringUtils'; const workspaceFolderToken = '${workspaceFolder}'; @@ -62,7 +63,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder); if (fastApiPath) { - fastApiPath = path.relative(folder.uri.fsPath, fastApiPath).replaceAll(path.sep, '.').replace('.py', ''); + fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPath), path.sep, '.').replace('.py', ''); providers.push({ name: 'Python: FastAPI', type: DebuggerTypeName, diff --git a/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts b/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts index e3c77a6b2d6d..992e910d8fc5 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -100,6 +100,9 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { // We're not sure if these envs were created for the workspace, so do not recommend them. return false; } + if (i.version?.major === 2) { + return false; + } return true; }); filteredInterpreters.sort(this.compare.bind(this)); diff --git a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 39965adec0a0..717e943bae84 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -32,6 +32,7 @@ import { IInterpreterQuickPick, IInterpreterQuickPickItem, IInterpreterSelector, + InterpreterQuickPickParams, IPythonPathUpdaterServiceManager, ISpecialQuickPickItem, } from '../../types'; @@ -127,12 +128,12 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem input: IMultiStepInput, state: InterpreterStateArgs, filter?: (i: PythonEnvironment) => boolean, - params?: { placeholder?: string | null; title?: string | null }, + params?: InterpreterQuickPickParams, ): Promise> { // If the list is refreshing, it's crucial to maintain sorting order at all // times so that the visible items do not change. const preserveOrderWhenFiltering = !!this.interpreterService.refreshPromise; - const suggestions = this._getItems(state.workspace, filter); + const suggestions = this._getItems(state.workspace, filter, params); state.path = undefined; const currentInterpreterPathDisplay = this.pathUtils.getDisplayName( this.configurationService.getSettings(state.workspace).pythonPath, @@ -183,10 +184,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem // Items are in the final state as all previous callbacks have finished executing. quickPick.busy = false; // Ensure we set a recommended item after refresh has finished. - this.updateQuickPickItems(quickPick, {}, state.workspace, filter); + this.updateQuickPickItems(quickPick, {}, state.workspace, filter, params); }); } - this.updateQuickPickItems(quickPick, event, state.workspace, filter); + this.updateQuickPickItems(quickPick, event, state.workspace, filter, params); }, }, }); @@ -208,14 +209,18 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return undefined; } - public _getItems(resource: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined): QuickPickType[] { + public _getItems( + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): QuickPickType[] { const suggestions: QuickPickType[] = [this.manualEntrySuggestion]; const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource); if (defaultInterpreterPathSuggestion) { suggestions.push(defaultInterpreterPathSuggestion); } - const interpreterSuggestions = this.getSuggestions(resource, filter); - this.finalizeItems(interpreterSuggestions, resource); + const interpreterSuggestions = this.getSuggestions(resource, filter, params); + this.finalizeItems(interpreterSuggestions, resource, params); suggestions.push(...interpreterSuggestions); return suggestions; } @@ -223,6 +228,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem private getSuggestions( resource: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, ): QuickPickType[] { const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); const items = this.interpreterSelector @@ -235,10 +241,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem const itemsWithFullName = this.interpreterSelector .getSuggestions(resource, true) .filter((i) => !filter || filter(i.interpreter)); - const recommended = this.interpreterSelector.getRecommendedSuggestion( - itemsWithFullName, - this.workspaceService.getWorkspaceFolder(resource)?.uri, - ); + let recommended: IInterpreterQuickPickItem | undefined; + if (!params?.skipRecommended) { + recommended = this.interpreterSelector.getRecommendedSuggestion( + itemsWithFullName, + this.workspaceService.getWorkspaceFolder(resource)?.uri, + ); + } if (recommended && items[0].interpreter.id === recommended.interpreter.id) { items.shift(); } @@ -289,10 +298,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem event: PythonEnvironmentsChangedEvent, resource: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, ) { // Active items are reset once we replace the current list with updated items, so save it. const activeItemBeforeUpdate = quickPick.activeItems.length > 0 ? quickPick.activeItems[0] : undefined; - quickPick.items = this.getUpdatedItems(quickPick.items, event, resource, filter); + quickPick.items = this.getUpdatedItems(quickPick.items, event, resource, filter, params); // Ensure we maintain the same active item as before. const activeItem = activeItemBeforeUpdate ? quickPick.items.find((item) => { @@ -317,6 +327,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem event: PythonEnvironmentsChangedEvent, resource: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, ): QuickPickType[] { const updatedItems = [...items.values()]; const areItemsGrouped = items.find((item) => isSeparatorItem(item)); @@ -364,16 +375,18 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem if (envIndex !== -1 && event.new === undefined) { updatedItems.splice(envIndex, 1); } - this.finalizeItems(updatedItems, resource); + this.finalizeItems(updatedItems, resource, params); return updatedItems; } - private finalizeItems(items: QuickPickType[], resource: Resource) { + private finalizeItems(items: QuickPickType[], resource: Resource, params?: InterpreterQuickPickParams) { const interpreterSuggestions = this.interpreterSelector.getSuggestions(resource, true); const r = this.interpreterService.refreshPromise; if (!r) { if (interpreterSuggestions.length) { - this.setRecommendedItem(interpreterSuggestions, items, resource); + if (!params?.skipRecommended) { + this.setRecommendedItem(interpreterSuggestions, items, resource); + } // Add warning label to certain environments items.forEach((item, i) => { if (isInterpreterQuickPickItem(item) && isProblematicCondaEnvironment(item.interpreter)) { @@ -513,7 +526,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem public async getInterpreterViaQuickPick( workspace: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined, - params?: { placeholder?: string | null; title?: string | null }, + params?: InterpreterQuickPickParams, ): Promise { const interpreterState: InterpreterStateArgs = { path: undefined, workspace }; const multiStep = this.multiStepFactory.create(); diff --git a/extensions/positron-python/src/client/interpreter/configuration/types.ts b/extensions/positron-python/src/client/interpreter/configuration/types.ts index 264ac523538d..bd0553d6762e 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/types.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/types.ts @@ -66,20 +66,26 @@ export interface IInterpreterComparer { getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined; } +export interface InterpreterQuickPickParams { + /** + * Specify `null` if a placeholder is not required. + */ + placeholder?: string | null; + /** + * Specify `null` if a title is not required. + */ + title?: string | null; + /** + * Specify `true` to skip showing recommended python interpreter. + */ + skipRecommended?: boolean; +} + export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); export interface IInterpreterQuickPick { getInterpreterViaQuickPick( workspace: Resource, filter?: (i: PythonEnvironment) => boolean, - params?: { - /** - * Specify `null` if a placeholder is not required. - */ - placeholder?: string | null; - /** - * Specify `null` if a title is not required. - */ - title?: string | null; - }, + params?: InterpreterQuickPickParams, ): Promise; } diff --git a/extensions/positron-python/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts b/extensions/positron-python/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts index da3d22a72bee..cf9175345cb0 100644 --- a/extensions/positron-python/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts +++ b/extensions/positron-python/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts @@ -6,7 +6,7 @@ import { ConfigurationTarget, Uri } from 'vscode'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; -import { IBrowserService, IPersistentStateFactory } from '../../common/types'; +import { IPersistentStateFactory } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { traceDecoratorError, traceError } from '../../logging'; import { EnvironmentType } from '../../pythonEnvironments/info'; @@ -22,7 +22,6 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { constructor( @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IBrowserService) private browserService: IBrowserService, @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, @inject(IPlatformService) private readonly platformService: IPlatformService, @@ -52,8 +51,8 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { if (!notificationPromptEnabled.value) { return; } - const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.moreInfo]; - const telemetrySelections: ['Yes', 'No', 'More Info'] = ['Yes', 'No', 'More Info']; + const prompts = [Common.allow, Common.close]; + const telemetrySelections: ['Allow', 'Close'] = ['Allow', 'Close']; const selection = await this.appShell.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts); sendTelemetryEvent(EventName.CONDA_INHERIT_ENV_PROMPT, undefined, { selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, @@ -67,8 +66,6 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { .update('integrated.inheritEnv', false, ConfigurationTarget.Global); } else if (selection === prompts[1]) { await notificationPromptEnabled.updateValue(false); - } else if (selection === prompts[2]) { - this.browserService.launch('https://aka.ms/AA66i8f'); } } diff --git a/extensions/positron-python/src/client/linters/baseLinter.ts b/extensions/positron-python/src/client/linters/baseLinter.ts index 3598d4ef1dfc..bb24bee1637f 100644 --- a/extensions/positron-python/src/client/linters/baseLinter.ts +++ b/extensions/positron-python/src/client/linters/baseLinter.ts @@ -9,6 +9,7 @@ import { IWorkspaceService } from '../common/application/types'; import { isTestExecution } from '../common/constants'; import '../common/extensions'; import { IPythonToolExecutionService } from '../common/process/types'; +import { splitLines } from '../common/stringUtils'; import { ExecutionInfo, Flake8CategorySeverity, @@ -184,7 +185,7 @@ export abstract class BaseLinter implements ILinter { _token: vscode.CancellationToken, regEx: string, ): Promise { - const outputLines = output.splitLines({ removeEmptyEntries: false, trim: false }); + const outputLines = splitLines(output, { removeEmptyEntries: false, trim: false }); return this.parseLines(outputLines, regEx); } diff --git a/extensions/positron-python/src/client/proposedApi.ts b/extensions/positron-python/src/client/proposedApi.ts index 5f40fcf263db..1b710a888c99 100644 --- a/extensions/positron-python/src/client/proposedApi.ts +++ b/extensions/positron-python/src/client/proposedApi.ts @@ -304,7 +304,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment const { path } = getEnvPath(env.executable.filename, env.location); const resolvedEnv: ResolvedEnvironment = { path, - id: getEnvID(path), + id: env.id!, executable: { uri: env.executable.filename === 'python' ? undefined : Uri.file(env.executable.filename), bitness: convertBitness(env.arch), diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts index 9340792a4f4b..2527f18202cd 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts @@ -198,6 +198,7 @@ function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Part return undefined; } return { + id: '', executable: { filename: env, sysPrefix: '', @@ -208,6 +209,7 @@ function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Part } if ('executablePath' in env) { return { + id: '', executable: { filename: env.executablePath, sysPrefix: '', @@ -235,7 +237,7 @@ export function getEnvPath(interpreterPath: string, envFolderPath?: string): Env } /** - * Gets unique identifier for an environment. + * Gets general unique identifier for most environments. */ export function getEnvID(interpreterPath: string, envFolderPath?: string): string { return normCasePath(getEnvPath(interpreterPath, envFolderPath).path); @@ -266,7 +268,15 @@ export function areSameEnv( const leftFilename = leftInfo.executable!.filename; const rightFilename = rightInfo.executable!.filename; + if (leftInfo.id && leftInfo.id === rightInfo.id) { + // In case IDs are available, use it. + return true; + } + if (getEnvID(leftFilename, leftInfo.location) === getEnvID(rightFilename, rightInfo.location)) { + // Otherwise use ID function to get the ID. Note ID returned by function may itself change if executable of + // an environment changes, for eg. when conda installs python into the env. So only use it as a fallback if + // ID is not available. return true; } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts index 565be30acf94..180e243ae710 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -7,7 +7,7 @@ import { createDeferred, Deferred, sleep } from '../../../common/utils/async'; import { createRunningWorkerPool, IWorkerPool, QueuePosition } from '../../../common/utils/workerPool'; import { getInterpreterInfo, InterpreterInformation } from './interpreter'; import { buildPythonExecInfo } from '../../exec'; -import { traceError, traceInfo } from '../../../logging'; +import { traceError, traceInfo, traceWarn } from '../../../logging'; import { Conda, CONDA_ACTIVATION_TIMEOUT, isCondaEnvironment } from '../../common/environmentManagers/conda'; import { PythonEnvInfo, PythonEnvKind } from '.'; import { normCasePath } from '../../common/externalDependencies'; @@ -37,8 +37,16 @@ export interface IEnvironmentInfoService { resetInfo(searchLocation: Uri): void; } -async function buildEnvironmentInfo(env: PythonEnvInfo): Promise { - const python = [env.executable.filename, '-I', OUTPUT_MARKER_SCRIPT]; +async function buildEnvironmentInfo( + env: PythonEnvInfo, + useIsolated = true, +): Promise { + const python = [env.executable.filename]; + if (useIsolated) { + python.push(...['-I', OUTPUT_MARKER_SCRIPT]); + } else { + python.push(...[OUTPUT_MARKER_SCRIPT]); + } const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(python, undefined, env.executable.filename)); return interpreterInfo; } @@ -134,7 +142,7 @@ class EnvironmentInfoService implements IEnvironmentInfoService { ); } - let reason: unknown; + let reason: Error | undefined; let r = await addToQueue(this.workerPool, env, priority).catch((err) => { reason = err; return undefined; @@ -161,6 +169,16 @@ class EnvironmentInfoService implements IEnvironmentInfoService { return undefined; }); } else if (reason) { + if (reason.message.includes('Unknown option: -I')) { + traceWarn(reason); + traceError( + 'Support for Python 2.7 has been dropped by the Python extension so certain features may not work, upgrade to using Python 3.', + ); + return buildEnvironmentInfo(env, false).catch((err) => { + traceError(err); + return undefined; + }); + } traceError(reason); } } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c0506d4a06ba..4bfcfac7fc87 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -15,7 +15,12 @@ import { import { buildEnvInfo, comparePythonVersionSpecificity, setEnvDisplayString, getEnvID } from '../../info/env'; import { getEnvironmentDirFromPath, getPythonVersionFromPath } from '../../../common/commonUtils'; import { arePathsSame, getFileInfo, isParentPath } from '../../../common/externalDependencies'; -import { AnacondaCompanyName, Conda, isCondaEnvironment } from '../../../common/environmentManagers/conda'; +import { + AnacondaCompanyName, + Conda, + getCondaInterpreterPath, + isCondaEnvironment, +} from '../../../common/environmentManagers/conda'; import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv'; import { Architecture, getOSType, OSType } from '../../../../common/utils/platform'; import { getPythonVersionFromPath as parsePythonVersionFromPath, parseVersion } from '../../info/pythonVersion'; @@ -57,7 +62,6 @@ export async function resolveBasicEnv(env: BasicEnvInfo): Promise await updateEnvUsingRegistry(resolvedEnv); } setEnvDisplayString(resolvedEnv); - resolvedEnv.id = getEnvID(resolvedEnv.executable.filename, resolvedEnv.location); const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); resolvedEnv.executable.ctime = ctime; resolvedEnv.executable.mtime = mtime; @@ -189,6 +193,13 @@ async function resolveCondaEnv(env: BasicEnvInfo): Promise { if (name) { info.name = name; } + if (env.envPath && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } return info; } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts index cb59c1e49a60..d589231cc7ca 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-continue */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -5,6 +6,7 @@ import { PythonEnvKind, PythonEnvSource } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator'; import { getRegistryInterpreters } from '../../../common/windowsUtils'; import { traceError } from '../../../../logging'; +import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; export class WindowsRegistryLocator extends Locator { public readonly providerId: string = 'windows-registry'; @@ -15,6 +17,12 @@ export class WindowsRegistryLocator extends Locator { const interpreters = await getRegistryInterpreters(); for (const interpreter of interpreters) { try { + // Filter out Microsoft Store app directories. We have a store app locator that handles this. + // The python.exe available in these directories might not be python. It can be a store install + // shortcut that takes you to microsoft store. + if (isMicrosoftStoreDir(interpreter.interpreterPath)) { + continue; + } const env: BasicEnvInfo = { kind: PythonEnvKind.OtherGlobal, executablePath: interpreter.interpreterPath, diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts index bdbcb7ea3ac5..969387f53a79 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -21,6 +21,7 @@ import { cache } from '../../../common/utils/decorators'; import { isTestExecution } from '../../../common/constants'; import { traceError, traceVerbose } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { splitLines } from '../../../common/stringUtils'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -185,7 +186,7 @@ export async function getPythonVersionFromConda(interpreterPath: string): Promis for (const configPath of configPaths) { if (await pathExists(configPath)) { try { - const lines = (await readFile(configPath)).splitLines(); + const lines = splitLines(await readFile(configPath)); // Sample data: // +defaults/linux-64::pip-20.2.4-py38_0 @@ -226,14 +227,11 @@ export async function getPythonVersionFromConda(interpreterPath: string): Promis /** * Return the interpreter's filename for the given environment. */ -async function getInterpreterPath(condaEnvironmentPath: string): Promise { +export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // where to find the Python binary within a conda env. const relativePath = getOSType() === OSType.Windows ? 'python.exe' : path.join('bin', 'python'); const filePath = path.join(condaEnvironmentPath, relativePath); - if (await pathExists(filePath)) { - return filePath; - } - return undefined; + return filePath; } // Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. @@ -494,8 +492,8 @@ export class Conda { */ // eslint-disable-next-line class-methods-use-this public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo | { prefix: string }): Promise { - const executablePath = await getInterpreterPath(condaEnv.prefix); - if (executablePath) { + const executablePath = getCondaInterpreterPath(condaEnv.prefix); + if (await pathExists(executablePath)) { traceVerbose('Found executable within conda env', JSON.stringify(condaEnv)); return executablePath; } diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts index 19e3bd80af59..48199b5bdc8f 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts @@ -19,6 +19,7 @@ import { StopWatch } from '../../../common/utils/stopWatch'; import { cache } from '../../../common/utils/decorators'; import { isTestExecution } from '../../../common/constants'; import { traceError, traceVerbose } from '../../../logging'; +import { splitLines } from '../../../common/stringUtils'; /** * Global virtual env dir for a project is named as: @@ -213,7 +214,7 @@ export class Poetry { */ const activated = '(Activated)'; const res = await Promise.all( - result.stdout.splitLines().map(async (line) => { + splitLines(result.stdout).map(async (line) => { if (line.endsWith(activated)) { line = line.slice(0, -activated.length); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts index 80a60a0580ca..78a018138e2b 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts @@ -4,6 +4,7 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import '../../../common/extensions'; +import { splitLines } from '../../../common/stringUtils'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info'; import { comparePythonVersionSpecificity } from '../../base/info/env'; @@ -136,7 +137,7 @@ export async function getPythonVersionFromPyvenvCfg(interpreterPath: string): Pr for (const configPath of configPaths) { if (await pathExists(configPath)) { try { - const lines = (await readFile(configPath)).splitLines(); + const lines = splitLines(await readFile(configPath)); const pythonVersions = lines .map((line) => { diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index ac77e51f22f4..91954c620c01 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -27,6 +27,7 @@ import { CONDA_ENV_CREATED_MARKER, CONDA_ENV_EXISTING_MARKER, } from './condaProgressAndTelemetry'; +import { splitLines } from '../../../common/stringUtils'; function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -112,7 +113,7 @@ async function createCondaEnv( let condaEnvPath: string | undefined; out.subscribe( (value) => { - const output = value.out.splitLines().join('\r\n'); + const output = splitLines(value.out).join('\r\n'); traceLog(output); if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { condaEnvPath = getCondaEnvFromOutput(output); diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 3bc927d898e9..850142372d2a 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -125,8 +125,14 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { workspace.uri, (i: PythonEnvironment) => [EnvironmentType.System, EnvironmentType.MicrosoftStore, EnvironmentType.Global].includes(i.envType), + { skipRecommended: true }, ); + if (!interpreter) { + traceError('Virtual env creation requires an interpreter.'); + return undefined; + } + let addGitIgnore = true; let installPackages = true; if (options) { @@ -139,11 +145,6 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { } const args = generateCommandArgs(installInfo, addGitIgnore); - if (!interpreter) { - traceError('Virtual env creation requires an interpreter.'); - return undefined; - } - if (!installInfo) { traceInfo('Virtual env creation exited during dependencies selection.'); return undefined; diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts index f1644c8597fa..234b2d1a8cb9 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -9,9 +9,9 @@ import { CancellationToken, QuickPickItem, RelativePattern, WorkspaceFolder } fr import { CreateEnv } from '../../../common/utils/localize'; import { showQuickPick } from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; -const exclude = '**/{.venv*,.git,.nox,.tox,.conda}/**'; +const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, token?: CancellationToken, @@ -25,20 +25,27 @@ async function getPipRequirementsFiles( return files; } -async function getTomlOptionalDeps(tomlPath: string): Promise { - const content = await fs.readFile(tomlPath, 'utf-8'); - const extras: string[] = []; +function tomlParse(content: string): tomljs.JsonMap { try { - const toml = tomljs.parse(content); - if (toml.project && (toml.project as Record>)['optional-dependencies']) { - const deps = (toml.project as Record>>)['optional-dependencies']; - for (const key of Object.keys(deps)) { - extras.push(key); - } - } + return tomljs.parse(content); } catch (err) { traceError('Failed to parse `pyproject.toml`:', err); } + return {}; +} + +function tomlHasBuildSystem(toml: tomljs.JsonMap): boolean { + return toml['build-system'] !== undefined; +} + +function getTomlOptionalDeps(toml: tomljs.JsonMap): string[] { + const extras: string[] = []; + if (toml.project && (toml.project as tomljs.JsonMap)['optional-dependencies']) { + const deps = (toml.project as tomljs.JsonMap)['optional-dependencies']; + for (const key of Object.keys(deps)) { + extras.push(key); + } + } return extras; } @@ -109,12 +116,15 @@ export async function pickPackagesToInstall( let extras: string[] = []; let tomlExists = false; + let hasBuildSystem = false; if (await fs.pathExists(tomlPath)) { tomlExists = true; - extras = await getTomlOptionalDeps(tomlPath); + const toml = tomlParse(await fs.readFile(tomlPath, 'utf-8')); + extras = getTomlOptionalDeps(toml); + hasBuildSystem = tomlHasBuildSystem(toml); } - if (tomlExists) { + if (tomlExists && hasBuildSystem) { if (extras.length === 0) { return { installType: 'toml', installList: [], source: tomlPath }; } @@ -125,6 +135,9 @@ export async function pickPackagesToInstall( } return undefined; } + if (tomlExists) { + traceInfo('Create env: Found toml without optional dependencies or build system.'); + } traceVerbose('Looking for pip requirements.'); const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => diff --git a/extensions/positron-python/src/client/pythonEnvironments/info/interpreter.ts b/extensions/positron-python/src/client/pythonEnvironments/info/interpreter.ts index 72ef670f2672..8925c7a6feb6 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/info/interpreter.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/info/interpreter.ts @@ -8,6 +8,7 @@ import { InterpreterInfoJson, } from '../../common/process/internal/scripts'; import { ShellExecFunc } from '../../common/process/types'; +import { replaceAll } from '../../common/stringUtils'; import { Architecture } from '../../common/utils/platform'; import { copyPythonExecInfo, PythonExecInfo } from '../exec'; @@ -68,7 +69,7 @@ export async function getInterpreterInfo( const argv = [info.command, ...info.args]; // Concat these together to make a set of quoted strings - const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replaceAll('\\', '\\\\')}"`), ''); + const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${replaceAll(c, '\\', '\\\\')}"`), ''); // Try shell execing the command, followed by the arguments. This will make node kill the process if it // takes too long. diff --git a/extensions/positron-python/src/client/telemetry/index.ts b/extensions/positron-python/src/client/telemetry/index.ts index c833922ace30..50947d8456e9 100644 --- a/extensions/positron-python/src/client/telemetry/index.ts +++ b/extensions/positron-python/src/client/telemetry/index.ts @@ -1310,11 +1310,10 @@ export interface IEventNamePropertyMapping { */ [EventName.CONDA_INHERIT_ENV_PROMPT]: { /** - * `Yes` When 'Yes' option is selected - * `No` When 'No' option is selected - * `More info` When 'More Info' option is selected + * `Yes` When 'Allow' option is selected + * `Close` When 'Close' option is selected */ - selection: 'Yes' | 'No' | 'More Info' | undefined; + selection: 'Allow' | 'Close' | undefined; }; /** * Telemetry event sent with details when user clicks the prompt with the following message: diff --git a/extensions/positron-python/src/client/testing/testController/common/server.ts b/extensions/positron-python/src/client/testing/testController/common/server.ts index adf5bba1a33c..48c0b81972af 100644 --- a/extensions/positron-python/src/client/testing/testController/common/server.ts +++ b/extensions/positron-python/src/client/testing/testController/common/server.ts @@ -71,6 +71,12 @@ export class PythonTestServer implements ITestServer, Disposable { return (this.server.address() as net.AddressInfo).port; } + public createUUID(cwd: string): string { + const uuid = crypto.randomUUID(); + this.uuids.set(uuid, cwd); + return uuid; + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -81,15 +87,13 @@ export class PythonTestServer implements ITestServer, Disposable { } async sendCommand(options: TestCommandOptions): Promise { - const uuid = crypto.randomUUID(); + const uuid = this.createUUID(options.cwd); const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, }; - this.uuids.set(uuid, options.cwd); - // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, diff --git a/extensions/positron-python/src/client/testing/testController/common/types.ts b/extensions/positron-python/src/client/testing/testController/common/types.ts index 064307ca8d9a..579c11d5ef25 100644 --- a/extensions/positron-python/src/client/testing/testController/common/types.ts +++ b/extensions/positron-python/src/client/testing/testController/common/types.ts @@ -151,6 +151,17 @@ export type TestCommandOptions = { testIds?: string[]; }; +export type TestCommandOptionsPytest = { + workspaceFolder: Uri; + cwd: string; + commandStr: string; + token?: CancellationToken; + outChannel?: OutputChannel; + debugBool?: boolean; + testIds?: string[]; + env: { [key: string]: string | undefined }; +}; + /** * Interface describing the server that will send test commands to the Python side, and process responses. * @@ -161,10 +172,14 @@ export interface ITestServer { readonly onDataReceived: Event; sendCommand(options: TestCommandOptions): Promise; serverReady(): Promise; + getPort(): number; + createUUID(cwd: string): string; } export interface ITestDiscoveryAdapter { + // ** Uncomment second line and comment out first line to use the new discovery method. discoverTests(uri: Uri): Promise; + // discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise } // interface for execution/runner adapter diff --git a/extensions/positron-python/src/client/testing/testController/controller.ts b/extensions/positron-python/src/client/testing/testController/controller.ts index 87ba15824281..7dbaa7019f6e 100644 --- a/extensions/positron-python/src/client/testing/testController/controller.ts +++ b/extensions/positron-python/src/client/testing/testController/controller.ts @@ -39,8 +39,10 @@ import { ITestExecutionAdapter, } from './common/types'; import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter'; -import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter'; +import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; +import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; +import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. @@ -141,7 +143,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc }); return this.refreshTestData(undefined, { forceRefresh: true }); }; - this.pythonTestServer = new PythonTestServer(this.pythonExecFactory, this.debugLauncher); } @@ -161,10 +162,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); testProvider = UNITTEST_PROVIDER; } else { - // TODO: PYTEST DISCOVERY ADAPTER - // this is a placeholder for now - discoveryAdapter = new UnittestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings }); - executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings }); + executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); testProvider = PYTEST_PROVIDER; } @@ -224,18 +223,30 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.refreshingStartedEvent.fire(); if (uri) { const settings = this.configSettings.getSettings(uri); + traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); if (settings.testing.pytestEnabled) { - traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); - // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - + // ** uncomment ~231 - 241 to NEW new test discovery mechanism + // const workspace = this.workspaceService.getWorkspaceFolder(uri); + // traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + // const testAdapter = + // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + // testAdapter.discoverTests( + // this.testController, + // this.refreshCancellation.token, + // this.testAdapters.size > 1, + // this.workspaceService.workspaceFile?.fsPath, + // this.pythonExecFactory, + // ); + // uncomment ~243 to use OLD test discovery mechanism await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); } else if (settings.testing.unittestEnabled) { - // TODO: Use new test discovery mechanism - // traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); + // ** Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + // uncomment ~248 - 258 to NEW new test discovery mechanism // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // console.warn(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + // traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); // const testAdapter = // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); // testAdapter.discoverTests( @@ -244,9 +255,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // this.testAdapters.size > 1, // this.workspaceService.workspaceFile?.fsPath, // ); - // // Ensure we send test telemetry if it gets disabled again - // this.sendTestDisabledTelemetry = true; - // comment below 229 to run the new way and uncomment above 212 ~ 227 + // uncomment ~260 to use OLD test discovery mechanism await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); } else { if (this.sendTestDisabledTelemetry) { @@ -375,7 +384,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } if (settings.testing.unittestEnabled) { - // potentially sqeeze in the new exeuction way here? + // potentially squeeze in the new execution way here? sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'unittest', debugging: request.profile?.kind === TestRunProfileKind.Debug, diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts new file mode 100644 index 000000000000..e2108b872845 --- /dev/null +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { Uri } from 'vscode'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { IConfigurationService } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { traceVerbose } from '../../../logging'; +import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; + +/** + * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied + */ +export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { + private deferred: Deferred | undefined; + + private cwd: string | undefined; + + constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + testServer.onDataReceived(this.onDataReceivedHandler, this); + } + + public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { + if (this.deferred && cwd === this.cwd) { + const testData: DiscoveredTestPayload = JSON.parse(data); + + this.deferred.resolve(testData); + this.deferred = undefined; + } + } + + // ** Old version of discover tests. + discoverTests(uri: Uri): Promise { + traceVerbose(uri); + this.deferred = createDeferred(); + return this.deferred.promise; + } + // Uncomment this version of the function discoverTests to use the new discovery method. + // public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + // const settings = this.configSettings.getSettings(uri); + // const { pytestArgs } = settings.testing; + // traceVerbose(pytestArgs); + + // this.cwd = uri.fsPath; + // return this.runPytestDiscovery(uri, executionFactory); + // } + + async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + if (!this.deferred) { + this.deferred = createDeferred(); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + const uuid = this.testServer.createUUID(uri.fsPath); + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + + const spawnOptions: SpawnOptions = { + cwd: uri.fsPath, + throwOnStdErr: true, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.testServer.getPort().toString(), + }, + }; + + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + try { + execService.exec( + ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), + spawnOptions, + ); + } catch (ex) { + console.error(ex); + } + } + return this.deferred.promise; + } +} diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts new file mode 100644 index 000000000000..35d62c50e774 --- /dev/null +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { + DataReceivedEvent, + ExecutionTestPayload, + ITestExecutionAdapter, + ITestServer, + TestCommandOptions, + TestExecutionCommand, +} from '../common/types'; + +/** + * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? + */ + +export class PytestTestExecutionAdapter implements ITestExecutionAdapter { + private deferred: Deferred | undefined; + + private cwd: string | undefined; + + constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + testServer.onDataReceived(this.onDataReceivedHandler, this); + } + + public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { + if (this.deferred && cwd === this.cwd) { + const testData: ExecutionTestPayload = JSON.parse(data); + + this.deferred.resolve(testData); + this.deferred = undefined; + } + } + + public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + if (!this.deferred) { + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + + const command = buildExecutionCommand(unittestArgs); + this.cwd = uri.fsPath; + + const options: TestCommandOptions = { + workspaceFolder: uri, + command, + cwd: this.cwd, + debugBool, + testIds, + }; + + this.deferred = createDeferred(); + + // send test command to server + // server fire onDataReceived event once it gets response + this.testServer.sendCommand(options); + } + return this.deferred.promise; + } +} + +function buildExecutionCommand(args: string[]): TestExecutionCommand { + const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + + return { + script: executionScript, + args: ['--udiscovery', ...args], + }; +} diff --git a/extensions/positron-python/src/client/testing/testController/unittest/runner.ts b/extensions/positron-python/src/client/testing/testController/unittest/runner.ts index ccc14ae0b4c2..d6bbb59ee640 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/runner.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/runner.ts @@ -4,6 +4,7 @@ import { injectable, inject, named } from 'inversify'; import { Location, TestController, TestItem, TestMessage, TestRun, TestRunProfileKind } from 'vscode'; import * as internalScripts from '../../../common/process/internal/scripts'; +import { splitLines } from '../../../common/stringUtils'; import { IOutputChannel } from '../../../common/types'; import { noop } from '../../../common/utils/misc'; import { traceError, traceInfo } from '../../../logging'; @@ -23,6 +24,10 @@ interface ITestData { subtest?: string; } +function getTracebackForOutput(traceback: string): string { + return splitLines(traceback, { trim: false, removeEmptyEntries: true }).join('\r\n'); +} + @injectable() export class UnittestRunner implements ITestsRunner { constructor( @@ -111,9 +116,7 @@ export class UnittestRunner implements ITestsRunner { runInstance.appendOutput(fixLogLines(text)); counts.passed += 1; } else if (data.outcome === 'failed' || data.outcome === 'passed-unexpected') { - const traceback = data.traceback - ? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n') - : ''; + const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; const text = `${rawTestCase.rawId} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; const message = new TestMessage(text); @@ -128,9 +131,7 @@ export class UnittestRunner implements ITestsRunner { stopTesting = true; } } else if (data.outcome === 'error') { - const traceback = data.traceback - ? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n') - : ''; + const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; const text = `${rawTestCase.rawId} Failed with Error: ${data.message}\r\n${traceback}\r\n`; const message = new TestMessage(text); @@ -145,9 +146,7 @@ export class UnittestRunner implements ITestsRunner { stopTesting = true; } } else if (data.outcome === 'skipped') { - const traceback = data.traceback - ? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n') - : ''; + const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; const text = `${rawTestCase.rawId} Skipped: ${data.message}\r\n${traceback}\r\n`; runInstance.skipped(testCase); runInstance.appendOutput(fixLogLines(text)); @@ -196,9 +195,7 @@ export class UnittestRunner implements ITestsRunner { if (data.subtest) { runInstance.appendOutput(fixLogLines(`${data.subtest} Failed\r\n`)); - const traceback = data.traceback - ? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n') - : ''; + const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; runInstance.appendOutput(fixLogLines(text)); @@ -226,9 +223,7 @@ export class UnittestRunner implements ITestsRunner { runInstance.errored(testCase, message); } } else if (data.outcome === 'error') { - const traceback = data.traceback - ? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n') - : ''; + const traceback = data.traceback ? getTracebackForOutput(data.traceback) : ''; const text = `${data.test} Failed with Error: ${data.message}\r\n${traceback}\r\n`; runInstance.appendOutput(fixLogLines(text)); } diff --git a/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts b/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts index 0ecab7649745..32bc0d5c29ef 100644 --- a/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts @@ -14,6 +14,7 @@ import { Uri, Location, } from 'vscode'; +import { splitLines } from '../../common/stringUtils'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; import { traceError } from '../../logging'; @@ -138,11 +139,12 @@ export class WorkspaceTestAdapter { rawTestExecData.result[keyTemp].outcome === 'subtest-failure' || rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' ) { - const traceback = rawTestExecData.result[keyTemp].traceback - ? rawTestExecData.result[keyTemp] - .traceback!.splitLines({ trim: false, removeEmptyEntries: true }) - .join('\r\n') - : ''; + const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${rawTestExecData.result[keyTemp].test} failed: ${ rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome }\r\n${traceback}\r\n`; @@ -196,6 +198,7 @@ export class WorkspaceTestAdapter { return Promise.resolve(); } + // add `executionFactory?: IPythonExecutionFactory,` to the function for new pytest method public async discoverTests( testController: TestController, token?: CancellationToken, @@ -216,8 +219,13 @@ export class WorkspaceTestAdapter { let rawTestData; try { + // ** First line is old way, section with if statement below is new way. rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); - + // if (executionFactory !== undefined) { + // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + // } else { + // traceVerbose('executionFactory is undefined'); + // } deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); @@ -352,6 +360,7 @@ function populateTestTree( testItem.canResolveChildren = false; testItem.range = range; testItem.tags = [RunTestTag, DebugTestTag]; + testRoot!.children.add(testItem); // add to our map wstAdapter.runIdToTestItem.set(child.runID, testItem); @@ -365,7 +374,6 @@ function populateTestTree( node.canResolveChildren = true; node.tags = [RunTestTag, DebugTestTag]; - testRoot!.children.add(node); } populateTestTree(testController, child, node, wstAdapter, token); diff --git a/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index bbca6c1a84e4..ea9bc9ae62d5 100644 --- a/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/extensions/positron-python/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -295,7 +295,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ), ) .returns(() => cmd) - .verifiable(typemoq.Times.once()); + .verifiable(typemoq.Times.exactly(2)); await diagnosticService.handle([diagnostic]); @@ -304,6 +304,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ { prompt: Common.selectPythonInterpreter, command: cmd }, + { prompt: Common.openOutputPanel, command: cmd }, ]); expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); }); diff --git a/extensions/positron-python/src/test/common/extensions.unit.test.ts b/extensions/positron-python/src/test/common/extensions.unit.test.ts index ffa75515fd9c..75d48024b2e8 100644 --- a/extensions/positron-python/src/test/common/extensions.unit.test.ts +++ b/extensions/positron-python/src/test/common/extensions.unit.test.ts @@ -102,20 +102,6 @@ suite('String Extensions', () => { expect(quotedString3.trimQuotes()).to.be.equal(expectedString); expect(quotedString4.trimQuotes()).to.be.equal(expectedString); }); - test('String should replace all substrings with new substring', () => { - const oldString = `foo \\ foo \\ foo`; - const expectedString = `foo \\\\ foo \\\\ foo`; - const oldString2 = `\\ foo \\ foo`; - const expectedString2 = `\\\\ foo \\\\ foo`; - const oldString3 = `\\ foo \\`; - const expectedString3 = `\\\\ foo \\\\`; - const oldString4 = `foo foo`; - const expectedString4 = `foo foo`; - expect(oldString.replaceAll('\\', '\\\\')).to.be.equal(expectedString); - expect(oldString2.replaceAll('\\', '\\\\')).to.be.equal(expectedString2); - expect(oldString3.replaceAll('\\', '\\\\')).to.be.equal(expectedString3); - expect(oldString4.replaceAll('\\', '\\\\')).to.be.equal(expectedString4); - }); }); suite('Array extensions', () => { diff --git a/extensions/positron-python/src/test/common/stringUtils.unit.test.ts b/extensions/positron-python/src/test/common/stringUtils.unit.test.ts new file mode 100644 index 000000000000..f8b5f2947631 --- /dev/null +++ b/extensions/positron-python/src/test/common/stringUtils.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import '../../client/common/extensions'; +import { replaceAll } from '../../client/common/stringUtils'; + +suite('String Extensions', () => { + test('String should replace all substrings with new substring', () => { + const oldString = `foo \\ foo \\ foo`; + const expectedString = `foo \\\\ foo \\\\ foo`; + const oldString2 = `\\ foo \\ foo`; + const expectedString2 = `\\\\ foo \\\\ foo`; + const oldString3 = `\\ foo \\`; + const expectedString3 = `\\\\ foo \\\\`; + const oldString4 = `foo foo`; + const expectedString4 = `foo foo`; + expect(replaceAll(oldString, '\\', '\\\\')).to.be.equal(expectedString); + expect(replaceAll(oldString2, '\\', '\\\\')).to.be.equal(expectedString2); + expect(replaceAll(oldString3, '\\', '\\\\')).to.be.equal(expectedString3); + expect(replaceAll(oldString4, '\\', '\\\\')).to.be.equal(expectedString4); + }); +}); diff --git a/extensions/positron-python/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts b/extensions/positron-python/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts index 6ae070721475..9499b5294d78 100644 --- a/extensions/positron-python/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts @@ -15,7 +15,7 @@ import { } from '../../../client/common/application/types'; import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IPlatformService } from '../../../client/common/platform/types'; -import { IBrowserService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; import { createDeferred, createDeferredFromPromise, sleep } from '../../../client/common/utils/async'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { IInterpreterService } from '../../../client/interpreter/contracts'; @@ -31,7 +31,6 @@ suite('Conda Inherit Env Prompt', async () => { let appShell: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let platformService: TypeMoq.IMock; - let browserService: TypeMoq.IMock; let applicationEnvironment: TypeMoq.IMock; let persistentStateFactory: IPersistentStateFactory; let notificationPromptEnabled: TypeMoq.IMock>; @@ -46,7 +45,6 @@ suite('Conda Inherit Env Prompt', async () => { setup(() => { workspaceService = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); - browserService = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); persistentStateFactory = mock(PersistentStateFactory); platformService = TypeMoq.Mock.ofType(); @@ -55,10 +53,8 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object, applicationEnvironment.object, ); @@ -67,10 +63,8 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), - platformService.object, applicationEnvironment.object, true, @@ -260,7 +254,6 @@ suite('Conda Inherit Env Prompt', async () => { setup(() => { workspaceService = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); - browserService = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); persistentStateFactory = mock(PersistentStateFactory); platformService = TypeMoq.Mock.ofType(); @@ -279,7 +272,6 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), @@ -305,7 +297,6 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), @@ -323,7 +314,6 @@ suite('Conda Inherit Env Prompt', async () => { setup(() => { workspaceService = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); - browserService = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); persistentStateFactory = mock(PersistentStateFactory); platformService = TypeMoq.Mock.ofType(); @@ -343,7 +333,6 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), @@ -363,7 +352,6 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), @@ -377,13 +365,12 @@ suite('Conda Inherit Env Prompt', async () => { }); suite('Method promptAndUpdate()', () => { - const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.moreInfo]; + const prompts = [Common.allow, Common.close]; setup(() => { workspaceService = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); persistentStateFactory = mock(PersistentStateFactory); - browserService = TypeMoq.Mock.ofType(); notificationPromptEnabled = TypeMoq.Mock.ofType>(); platformService = TypeMoq.Mock.ofType(); applicationEnvironment = TypeMoq.Mock.ofType(); @@ -394,7 +381,6 @@ suite('Conda Inherit Env Prompt', async () => { condaInheritEnvPrompt = new CondaInheritEnvPrompt( interpreterService.object, workspaceService.object, - browserService.object, appShell.object, instance(persistentStateFactory), @@ -439,16 +425,11 @@ suite('Conda Inherit Env Prompt', async () => { .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); - browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); verifyAll(); workspaceConfig.verifyAll(); notificationPromptEnabled.verifyAll(); - browserService.verifyAll(); }); test('Update terminal settings if `Yes` is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); @@ -458,7 +439,7 @@ suite('Conda Inherit Env Prompt', async () => { .verifiable(TypeMoq.Times.once()); appShell .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) + .returns(() => Promise.resolve(Common.allow)) .verifiable(TypeMoq.Times.once()); workspaceService .setup((ws) => ws.getConfiguration('terminal')) @@ -472,16 +453,11 @@ suite('Conda Inherit Env Prompt', async () => { .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); - browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); verifyAll(); workspaceConfig.verifyAll(); notificationPromptEnabled.verifyAll(); - browserService.verifyAll(); }); test('Disable notification prompt if `No` is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); @@ -491,40 +467,7 @@ suite('Conda Inherit Env Prompt', async () => { .verifiable(TypeMoq.Times.once()); appShell .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) - .returns(() => Promise.resolve(Common.bannerLabelNo)) - .verifiable(TypeMoq.Times.once()); - workspaceService - .setup((ws) => ws.getConfiguration('terminal')) - .returns(() => workspaceConfig.object) - .verifiable(TypeMoq.Times.never()); - workspaceConfig - .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - notificationPromptEnabled - .setup((n) => n.updateValue(false)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - await condaInheritEnvPrompt.promptAndUpdate(); - verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); - verifyAll(); - workspaceConfig.verifyAll(); - notificationPromptEnabled.verifyAll(); - browserService.verifyAll(); - }); - test('Launch browser if `More info` option is selected', async () => { - const workspaceConfig = TypeMoq.Mock.ofType(); - notificationPromptEnabled - .setup((n) => n.value) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) - .returns(() => Promise.resolve(Common.moreInfo)) + .returns(() => Promise.resolve(Common.close)) .verifiable(TypeMoq.Times.once()); workspaceService .setup((ws) => ws.getConfiguration('terminal')) @@ -537,17 +480,12 @@ suite('Conda Inherit Env Prompt', async () => { notificationPromptEnabled .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) - .returns(() => undefined) .verifiable(TypeMoq.Times.once()); await condaInheritEnvPrompt.promptAndUpdate(); verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); verifyAll(); workspaceConfig.verifyAll(); notificationPromptEnabled.verifyAll(); - browserService.verifyAll(); }); }); }); diff --git a/extensions/positron-python/src/test/linters/lint.multiroot.test.ts b/extensions/positron-python/src/test/linters/lint.multiroot.test.ts index f1eaa0bdf803..f89ee86c0b42 100644 --- a/extensions/positron-python/src/test/linters/lint.multiroot.test.ts +++ b/extensions/positron-python/src/test/linters/lint.multiroot.test.ts @@ -12,6 +12,14 @@ import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, Product, ProductType } from '../../client/common/types'; import { OSType } from '../../client/common/utils/platform'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { + IPythonPathUpdaterServiceManager, + IPythonPathUpdaterServiceFactory, +} from '../../client/interpreter/configuration/types'; +import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; +import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; import { ILinter, ILinterManager } from '../../client/linters/types'; import { isOs } from '../common'; import { TEST_TIMEOUT } from '../constants'; @@ -25,22 +33,22 @@ suite('Multiroot Linting', () => { const flake8Setting = 'linting.flake8Enabled'; let ioc: UnitTestIocContainer; - suiteSetup(function () { + suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { this.skip(); } - return initialize(); - }); - setup(async () => { + await initialize(); await initializeDI(); await initializeTest(); }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await ioc.dispose(); + suiteTeardown(async () => { + await ioc?.dispose(); await closeActiveWindows(); PythonSettings.dispose(); }); + teardown(async () => { + await closeActiveWindows(); + }); async function initializeDI() { ioc = new UnitTestIocContainer(); @@ -50,6 +58,18 @@ suite('Multiroot Linting', () => { ioc.registerVariableTypes(); ioc.registerFileSystemTypes(); await ioc.registerMockInterpreterTypes(); + ioc.serviceManager.addSingleton( + IActivatedEnvironmentLaunch, + ActivatedEnvironmentLaunch, + ); + ioc.serviceManager.addSingleton( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService, + ); + ioc.serviceManager.addSingleton( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory, + ); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); ioc.serviceManager.addSingleton( diff --git a/extensions/positron-python/src/test/proposedApi.unit.test.ts b/extensions/positron-python/src/test/proposedApi.unit.test.ts index 80db62f4814b..5dfa54492c6b 100644 --- a/extensions/positron-python/src/test/proposedApi.unit.test.ts +++ b/extensions/positron-python/src/test/proposedApi.unit.test.ts @@ -252,6 +252,7 @@ suite('Proposed Extension API', () => { test('environments: python found', async () => { const expectedEnvs = [ { + id: normCasePath('this/is/a/test/python/path1'), executable: { filename: 'this/is/a/test/python/path1', ctime: 1, @@ -273,6 +274,7 @@ suite('Proposed Extension API', () => { }, }, { + id: normCasePath('this/is/a/test/python/path2'), executable: { filename: 'this/is/a/test/python/path2', ctime: 1, @@ -297,6 +299,7 @@ suite('Proposed Extension API', () => { const envs = [ ...expectedEnvs, { + id: normCasePath('this/is/a/test/python/path3'), executable: { filename: 'this/is/a/test/python/path3', ctime: 1, @@ -343,6 +346,7 @@ suite('Proposed Extension API', () => { searchLocation: Uri.file(workspacePath), }), { + id: normCasePath('this/is/a/test/python/path1'), executable: { filename: 'this/is/a/test/python/path1', ctime: 1, @@ -364,6 +368,7 @@ suite('Proposed Extension API', () => { }, }, { + id: normCasePath('this/is/a/test/python/path2'), executable: { filename: 'this/is/a/test/python/path2', ctime: 1, diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 856733b29a1c..57e57fb1303d 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -62,28 +62,36 @@ suite('venv Creation provider tests', () => { test('No workspace selected', async () => { pickWorkspaceFolderStub.resolves(undefined); + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); assert.isUndefined(await venvProvider.createEnvironment()); assert.isTrue(pickWorkspaceFolderStub.calledOnce); + interpreterQuickPick.verifyAll(); + assert.isTrue(pickPackagesToInstallStub.notCalled); }); test('No Python selected', async () => { pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(typemoq.Times.once()); assert.isUndefined(await venvProvider.createEnvironment()); + + assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); + assert.isTrue(pickPackagesToInstallStub.notCalled); }); test('User pressed Esc while selecting dependencies', async () => { pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); @@ -97,7 +105,7 @@ suite('venv Creation provider tests', () => { pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); @@ -160,7 +168,7 @@ suite('venv Creation provider tests', () => { pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); @@ -217,7 +225,7 @@ suite('venv Creation provider tests', () => { pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index 5627feee598d..5ef001c985ad 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -46,11 +46,26 @@ suite('Venv Utils test', () => { }); }); - test('Toml found with no optional deps', async () => { + test('Toml found with no build system', async () => { findFilesStub.resolves([]); pathExistsStub.resolves(true); readFileStub.resolves('[project]\nname = "spam"\nversion = "2020.0.0"\n'); + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickStub.notCalled); + assert.deepStrictEqual(actual, { + installType: 'none', + installList: [], + }); + }); + + test('Toml found with no optional deps', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]', + ); + const actual = await pickPackagesToInstall(workspace1); assert.isTrue(showQuickPickStub.notCalled); assert.deepStrictEqual(actual, { @@ -64,7 +79,7 @@ suite('Venv Utils test', () => { findFilesStub.resolves([]); pathExistsStub.resolves(true); readFileStub.resolves( - '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', ); showQuickPickStub.resolves(undefined); @@ -88,7 +103,7 @@ suite('Venv Utils test', () => { findFilesStub.resolves([]); pathExistsStub.resolves(true); readFileStub.resolves( - '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', ); showQuickPickStub.resolves([]); @@ -116,7 +131,7 @@ suite('Venv Utils test', () => { findFilesStub.resolves([]); pathExistsStub.resolves(true); readFileStub.resolves( - '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', ); showQuickPickStub.resolves([{ label: 'doc' }]); @@ -144,7 +159,7 @@ suite('Venv Utils test', () => { findFilesStub.resolves([]); pathExistsStub.resolves(true); readFileStub.resolves( - '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]\ncov = ["pytest-cov"]', + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]\ncov = ["pytest-cov"]', ); showQuickPickStub.resolves([{ label: 'test' }, { label: 'cov' }]);