From 7dd0e33eafba4c404de870c4d3423bf4ab05da61 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 11 Dec 2023 15:22:55 +0000 Subject: [PATCH] Merged PR posit-dev/positron-python#277: Merge vscode-python 2023.22 from upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge pull request #277 from posit-dev/merge/2023.22 Merge vscode-python 2023.22 from upstream -------------------- Commit message for posit-dev/positron-python@2e3ea32dc9bd94a184696e858e911710d1269f2e: handle new upstream `deprecated` response upon parser error -------------------- Commit message for posit-dev/positron-python@d50c32694b76c68d06ff492264b42735a69f0151: Update yarn.lock -------------------- Commit message for posit-dev/positron-python@105bf9d46e04f84c6ceda0e5a1f3760efac7f523: Merge commit 'a0cddbdf0939729cdb82dca2d129433ad475f51e' -------------------- Commit message for microsoft/vscode-python@a0cddbdf0939729cdb82dca2d129433ad475f51e: bump-release-2023.22 (microsoft/vscode-python#22587) bump to release version 2023.22 and update package.json -------------------- Commit message for microsoft/vscode-python@5302d0e94d808358cfcf541822edcfb204a08c9d: Revert unsuccessful REPL decoration fix and disable for windows (microsoft/vscode-python#22578) Reverting microsoft/vscode-python#22572 since it is producing color decoration on weird Python REPL spot for Windows pwsh users, and disable decoration entirely for Windows pwsh users temporarily while trying to fix specific problem. Disable feature for stable. Refer: microsoft/vscode-python#22546 microsoft/vscode-python#22535 -------------------- Commit message for microsoft/vscode-python@bffc9b36e414e8f4919f09060f95196e1dabe05f: Fix shell integration decoration bug for Windows pwsh (microsoft/vscode-python#22572) Attempt to fix colorless and randomized circle decoration for Python REPL usage by Windows Powershell users. Resolves: microsoft/vscode-python#22546 microsoft/vscode-python#22535 -------------------- Commit message for microsoft/vscode-python@e1b54d01ad87d077628bfc810fe994ac2f9088b3: fix no test id provided for pytest run (microsoft/vscode-python#22569) fixes https://github.com/microsoft/vscode-python/issues/22567 Now if a user tries to click the "run" button a empty folder (ie a folder with no tests in it) it will just run pytest with no testIds as args and notify the user. -------------------- Commit message for microsoft/vscode-python@61fcf3a2d3f3a7ec6b17ecba8a23cd30a6c264e2: Properly build deactivate commands for powershell (microsoft/vscode-python#22570) For microsoft/vscode-python#20950 -------------------- Commit message for microsoft/vscode-python@db6e15e2214629fb2c09b8bf82c39ab53ebd7c30: Do not throw if initializing deactivate script fails (microsoft/vscode-python#22566) For microsoft/vscode-python#20950 -------------------- Commit message for microsoft/vscode-python@caa50dcf146219e9233f7d17a5d859d345baca91: Fix shell integration decoration for windows pwsh (microsoft/vscode-python#22560) Attempting to fix shell integration decoration glitch for windows pwsh when using Python REPL from VS Code. Resolves: microsoft/vscode-python#22546 microsoft/vscode-python#22535 -------------------- Commit message for microsoft/vscode-python@3efb54fe5137ac4915d8d3e3e9fce7a2bc658f20: White space after REPL prompt (microsoft/vscode-python#22549) There should be white space after new REPL prompt that I override for purpose of shell integration. Resolves: microsoft/vscode-python#22548 -------------------- Commit message for microsoft/vscode-python@261ae66bd27ac81fd9751bac6ad56c2f54d3d33b: Improvement: Pass along python interpreter to jedi-language-server (microsoft/vscode-python#22466) Fixes https://github.com/microsoft/vscode-python/issues/22495 ## Before this PR Overriding the Python interpreter to a different environment that isn't the current globally-activated base environment would yield no completions when Jedi is used as the underlying language server. Example [stackoverflow question](https://stackoverflow.com/questions/62018436/vscode-intellisense-code-completion-doesnt-work-when-i-am-not-in-base-conda-e) hitting the same issue. ## After this PR We now pass along the interpreter path to jedi-language-server as part of the initial options under `workspace.environmentPath` ([ref](https://github.com/pappasam/jedi-language-server/#workspaceenvironmentpath)) -------------------- Commit message for microsoft/vscode-python@1639753185a54a50f142068d26d2f2e5a3690488: switch to conditional error message on check for UUID and port during pytest config (microsoft/vscode-python#22534) fixes https://github.com/microsoft/vscode-python/issues/19857 -------------------- Commit message for microsoft/vscode-python@9a5363ce0e1a64275ac7fdc8e1cf0ac95038ec61: Resolve test suite discovery import errors due to path ordering (microsoft/vscode-python#22454) [Issue microsoft/vscode-python#22453](https://github.com/microsoft/vscode-python/issues/22453) - Once starting to run discovery, add the projects root path to PATH at index 0 so that any further imports will use the projects root directory and not reference the incorrect directory. - Since the test suite only allows the start_dir to be one directory deep, we can conclude that if the start_dir is not "." or contains a "/", that we need to add that start_dir's parent to PATH. Otherwise, we simply add the start_dir to PATH. -------------------- Commit message for microsoft/vscode-python@eb96141dc8540f2b6838f7fab278a86481575ede: Use shell integration to denote success/failure (microsoft/vscode-python#22487) Resolves: microsoft/vscode-python#22486 Use shell integration to denote success/failure in Python REPL launched from VS Code. This would mean having the blue or red decorators based on whether or not user's command succeeded. -------------------- Commit message for microsoft/vscode-python@f6e13383b24c4d1b2a72ad1066a4a1762fbc1055: Use worker threads for fetching conda environments and interpreter related info (microsoft/vscode-python#22481) -------------------- Commit message for microsoft/vscode-python@3c552f9d636b9fdd04de3e34e1b435ceb2b951b3: Show warning and allow user to turn off smart send for deprecated Python code (microsoft/vscode-python#22353) Resolves: microsoft/vscode-python#22341 microsoft/vscode-python#22340 Showing warning message after detecting user is on Python file with deprecated Python code, and are attempting to run smart send via shift+enter action. Allow user to turn off this via workspace setting. --------- Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Co-authored-by: Kartik Raj -------------------- Commit message for microsoft/vscode-python@ef983f497e3565480cbc85ef265f34035fd43395: Bump `vscode-tas-client` and fix tests (microsoft/vscode-python#22493) Closes https://github.com/microsoft/vscode-python/pull/22461 Sinon requires the actual object definition, so we need to point it to the module file that actually has the function. -------------------- Commit message for microsoft/vscode-python@9298b2c4fc8dc1277e2208f51b2beca4d34ca88e: Bump dessant/lock-threads from 4 to 5 (microsoft/vscode-python#22474) Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 4 to 5.
Release notes

Sourced from dessant/lock-threads's releases.

v5.0.0

Learn more about this release from the changelog.

v4.0.1

Learn more about this release from the changelog.

Changelog

Sourced from dessant/lock-threads's changelog.

5.0.0 (2023-11-14)

⚠ BREAKING CHANGES

  • Discussions are also processed by default, set the process-only input parameter to preserve the old behavior
    steps:
      - uses: dessant/lock-threads@v5
        with:
          process-only: 'issues, prs'
  • the action now requires Node.js 20

Features

Bug Fixes

4.0.1 (2023-06-12)

Bug Fixes

  • retry and throttle GitHub API requests (1618e91), closes #35

4.0.0 (2022-12-04)

⚠ BREAKING CHANGES

  • the action now requires Node.js 16

Bug Fixes

3.0.0 (2021-09-27)

⚠ BREAKING CHANGES

  • input parameter names have changed

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dessant/lock-threads&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@e27185a2ac31edc470a881ab6cbf555ec7f66ee6: Use worker threads for fetching Windows Registry interpreters (microsoft/vscode-python#22479) For https://github.com/microsoft/vscode-python/issues/22146 -------------------- Commit message for microsoft/vscode-python@7a4de92b8e0db9ff73228b3653ed51485d0b7fcf: Use `WorkspaceFolder.name` in addition to workspace folder's basename when resolving `${workspaceFolder:name}` syntax in settings (microsoft/vscode-python#22471) Closes https://github.com/microsoft/vscode-python/issues/22452 -------------------- Commit message for microsoft/vscode-python@fbfa12630551bb00cfb6a61539531ef71f48bcec: Add 3.12 to conda version list (microsoft/vscode-python#22470) -------------------- Commit message for microsoft/vscode-python@47552a31f88a74c83874982675291018de7cd62a: Install requirements before toml (microsoft/vscode-python#22468) Fixes https://github.com/microsoft/vscode-python/issues/22423 -------------------- Commit message for microsoft/vscode-python@a1fac8125f4841a14c8a8aa599c0f360873b17c3: If active editor opened is outside the editor, activate the first workspace (microsoft/vscode-python#22450) For https://github.com/microsoft/vscode-python/issues/22449 -------------------- Commit message for microsoft/vscode-python@b68ddee506cb8330e12c27e9c6421e1e0430763a: Support deactivating virtual environments without user intervention (microsoft/vscode-python#22405) Closes https://github.com/microsoft/vscode-python/issues/22448 Adds deactivate script to `PATH` -------------------- Commit message for microsoft/vscode-python@8d174a821a312b7498b92fbae2336c6d885c95dc: Fix shell integration API reliability (microsoft/vscode-python#22446) https://github.com/microsoft/vscode-python/pull/22440 It leads to terminals activating forever. -------------------- Commit message for microsoft/vscode-python@1b3c1ea2914c157fb4205f87be09501caabd7e9a: Adding `Locked` comments to walkthrough commands (microsoft/vscode-python#22432) Adds comments to lock command names for walkthrough localization. Only added Locked comments for walkthrough tiles that are in use and have commands -------------------- Commit message for microsoft/vscode-python@2fc9fea46f073b43679ab32ba666ee48ec3a8530: Revert "Reliably detect whether shell integration is working" (microsoft/vscode-python#22445) Reverts microsoft/vscode-python#22440 It seems reactivating never finishes after this, although this doesn't repro when debugging the extension, have to investigate further. -------------------- Commit message for microsoft/vscode-python@f98caf64bdcd79e0594526ae92db2151385dadc7: Add consoleTitle to launch.json properties schema (microsoft/vscode-python#22406) consoleTitle is effectively an undocumented feature. pydebug accepts the config key, but vscode will report a problem because it is missing from the schema. see [this debugpy issue](https://github.com/microsoft/debugpy/issues/1178) and [#13040](https://github.com/microsoft/vscode-python/issues/13040) I used the phrase "console or terminal" because I noticed the name was represented on both integratedTerminal and internalConsole. I didn't test externalTerminal. --------- Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> -------------------- Commit message for microsoft/vscode-python@2a976e361dd595a5b63346de96e2ba8941ef498b: Reliably detect whether shell integration is working (microsoft/vscode-python#22440) Closes https://github.com/microsoft/vscode-python/issues/22439 Blocked on https://github.com/microsoft/vscode-distro/pull/822 -------------------- Commit message for microsoft/vscode-python@67b480105fa90f07fc1f8b3de33309c7546026a0: Improve shell identifier on case-insensitive system (microsoft/vscode-python#22391) Use case-insensitive regex to remove `.exe` extension. See: https://github.com/microsoft/vscode-python/issues/22036#issuecomment-1786354934 -------------------- Commit message for microsoft/vscode-python@7bc45e57253b89b28cd5c7eaa9197ac70ef42b7f: Do not attempt to kill the process if it has already exited (microsoft/vscode-python#22424) Closes https://github.com/microsoft/vscode-python/issues/22420 This bugs seems to have existed every since `rawProcessApi.ts` was created. `proc.killed` can be `false` even after process has exited. -------------------- Commit message for microsoft/vscode-python@f6cfa6e2cc89ab37f7d57096e19d3a9a77e441cf: Remove old code for folder support in interpreter path setting (microsoft/vscode-python#22413) Closes https://github.com/microsoft/vscode-python/issues/12452 Lead-authored-by: Kartik Raj Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Co-authored-by: John H. Ayad Co-authored-by: Wasim Lorgat Co-authored-by: Shane Saravia Co-authored-by: Trysten Co-authored-by: JamzumSum Co-authored-by: Eleanor Boyd Co-authored-by: Karthik Nadig Co-authored-by: Pete Farland Co-authored-by: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Signed-off-by: GitHub --- .../.github/workflows/build.yml | 2 +- .../.github/workflows/lock-issues.yml | 2 +- .../.github/workflows/pr-check.yml | 6 +- extensions/positron-python/.gitignore | 1 + .../positron-python/.vscode/settings.json | 2 +- .../build/webpack/webpack.extension.config.js | 8 + extensions/positron-python/package.json | 20 +- extensions/positron-python/package.nls.json | 68 ++++- .../pythonFiles/create_venv.py | 8 +- .../pythonFiles/deactivate.csh | 6 - .../pythonFiles/deactivate.fish | 36 --- .../pythonFiles/deactivate.ps1 | 31 -- .../pythonFiles/deactivate/bash/deactivate | 44 +++ .../pythonFiles/deactivate/fish/deactivate | 44 +++ .../deactivate/powershell/deactivate.ps1 | 11 + .../{ => deactivate/zsh}/deactivate | 17 +- .../pythonFiles/normalizeSelection.py | 47 ++- .../pythonFiles/printEnvVariablesToFile.py | 11 +- .../positron-python/pythonFiles/pythonrc.py | 55 ++++ .../pythonFiles/tests/test_create_venv.py | 24 +- .../tests/test_shell_integration.py | 48 ++++ .../pythonFiles/unittestadapter/discovery.py | 5 + .../pythonFiles/vscode_pytest/__init__.py | 13 +- .../vscode_pytest/run_pytest_script.py | 8 + .../client/activation/activationManager.ts | 5 +- .../client/activation/jedi/analysisOptions.ts | 4 + .../common/application/applicationShell.ts | 10 +- .../src/client/common/application/commands.ts | 1 + .../src/client/common/application/types.ts | 34 +++ .../src/client/common/configSettings.ts | 71 +---- .../src/client/common/constants.ts | 3 +- .../src/client/common/experiments/groups.ts | 4 + .../src/client/common/process/proc.ts | 9 +- .../client/common/process/rawProcessApis.ts | 17 +- .../src/client/common/process/types.ts | 3 +- .../src/client/common/process/worker/main.ts | 42 +++ .../common/process/worker/plainExec.worker.ts | 16 ++ .../process/worker/rawProcessApiWrapper.ts | 26 ++ .../common/process/worker/shellExec.worker.ts | 16 ++ .../src/client/common/process/worker/types.ts | 38 +++ .../process/worker/workerRawProcessApis.ts | 213 ++++++++++++++ .../src/client/common/terminal/service.ts | 8 +- .../client/common/terminal/shellDetector.ts | 5 +- .../shellDetectors/baseShellDetector.ts | 5 +- .../shellDetectors/settingsShellDetector.ts | 2 - .../userEnvironmentShellDetector.ts | 2 - .../src/client/common/types.ts | 5 + .../src/client/common/utils/async.ts | 34 +++ .../src/client/common/utils/decorators.ts | 3 +- .../src/client/common/utils/localize.ts | 9 +- .../common/variables/systemVariables.ts | 2 + .../client/interpreter/activation/service.ts | 13 +- .../virtualEnvs/activatedEnvLaunch.ts | 5 +- .../src/client/positron/statementRange.ts | 8 + .../composite/envsCollectionService.ts | 3 +- .../base/locators/composite/resolverUtils.ts | 2 +- .../base/locators/lowLevel/condaLocator.ts | 9 +- .../lowLevel/windowsKnownPathsLocator.ts | 15 +- .../lowLevel/windowsRegistryLocator.ts | 9 +- .../common/environmentManagers/conda.ts | 26 +- .../common/environmentManagers/pipenv.ts | 4 +- .../common/externalDependencies.ts | 18 +- .../common/registryKeys.worker.ts | 24 ++ .../common/registryValues.worker.ts | 27 ++ .../common/windowsRegistry.ts | 57 ++-- .../pythonEnvironments/common/windowsUtils.ts | 26 +- .../creation/provider/condaUtils.ts | 4 +- .../codeExecution/djangoShellCodeExecution.ts | 1 + .../client/terminals/codeExecution/helper.ts | 19 +- .../client/terminals/codeExecution/repl.ts | 4 +- .../codeExecution/terminalCodeExecution.ts | 19 +- .../deactivatePrompt.ts | 201 ------------- .../deactivateScripts.ts | 108 ------- .../deactivateService.ts | 102 +++++++ .../envCollectionActivation/service.ts | 50 ++-- .../shellIntegration.ts | 13 - .../shellIntegrationService.ts | 85 ++++++ .../src/client/terminals/serviceRegistry.ts | 11 +- .../src/client/terminals/types.ts | 11 + .../activation/activationManager.unit.test.ts | 25 ++ .../jedi/jediAnalysisOptions.unit.test.ts | 21 ++ .../common/experiments/service.unit.test.ts | 10 +- .../src/test/common/process/proc.exec.test.ts | 22 ++ .../common/terminals/service.unit.test.ts | 33 +++ .../shellDetectors.unit.test.ts | 1 + ...rminalEnvVarCollectionService.unit.test.ts | 91 +++++- .../lowLevel/condaLocator.testvirtualenvs.ts | 34 ++- .../lowLevel/condaLocator.unit.test.ts | 4 +- .../windowsRegistryLocator.testvirtualenvs.ts | 28 ++ .../windowsRegistryLocator.unit.test.ts | 2 + .../environmentManagers/conda.unit.test.ts | 5 + .../sample_invalid_smart_selection.py | 10 + .../terminals/codeExecution/smartSend.test.ts | 93 +++++- .../terminalCodeExec.unit.test.ts | 2 + .../deactivatePrompt.unit.test.ts | 271 ------------------ .../terminals/serviceRegistry.unit.test.ts | 8 +- ....proposed.terminalExecuteCommandEvent.d.ts | 45 +++ extensions/positron-python/yarn.lock | 53 ++-- 98 files changed, 1779 insertions(+), 957 deletions(-) delete mode 100644 extensions/positron-python/pythonFiles/deactivate.csh delete mode 100644 extensions/positron-python/pythonFiles/deactivate.fish delete mode 100644 extensions/positron-python/pythonFiles/deactivate.ps1 create mode 100755 extensions/positron-python/pythonFiles/deactivate/bash/deactivate create mode 100755 extensions/positron-python/pythonFiles/deactivate/fish/deactivate create mode 100644 extensions/positron-python/pythonFiles/deactivate/powershell/deactivate.ps1 rename extensions/positron-python/pythonFiles/{ => deactivate/zsh}/deactivate (61%) mode change 100644 => 100755 create mode 100644 extensions/positron-python/pythonFiles/pythonrc.py create mode 100644 extensions/positron-python/pythonFiles/tests/test_shell_integration.py create mode 100644 extensions/positron-python/src/client/common/process/worker/main.ts create mode 100644 extensions/positron-python/src/client/common/process/worker/plainExec.worker.ts create mode 100644 extensions/positron-python/src/client/common/process/worker/rawProcessApiWrapper.ts create mode 100644 extensions/positron-python/src/client/common/process/worker/shellExec.worker.ts create mode 100644 extensions/positron-python/src/client/common/process/worker/types.ts create mode 100644 extensions/positron-python/src/client/common/process/worker/workerRawProcessApis.ts create mode 100644 extensions/positron-python/src/client/pythonEnvironments/common/registryKeys.worker.ts create mode 100644 extensions/positron-python/src/client/pythonEnvironments/common/registryValues.worker.ts delete mode 100644 extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts delete mode 100644 extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts create mode 100644 extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateService.ts delete mode 100644 extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts create mode 100644 extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegrationService.ts create mode 100644 extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.testvirtualenvs.ts create mode 100644 extensions/positron-python/src/test/pythonFiles/terminalExec/sample_invalid_smart_selection.py delete mode 100644 extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts create mode 100644 extensions/positron-python/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index d17d5fff4b9..2a155b5ebf9 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -282,7 +282,7 @@ jobs: shell: pwsh if: matrix.test-suite == 'venv' run: | - # 1. For `terminalActivation.testvirtualenvs.test.ts` + # 1. For `*.testvirtualenvs.test.ts` if ('${{ matrix.os }}' -match 'windows-latest') { $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda diff --git a/extensions/positron-python/.github/workflows/lock-issues.yml b/extensions/positron-python/.github/workflows/lock-issues.yml index bcd9ea267f9..47f243d7197 100644 --- a/extensions/positron-python/.github/workflows/lock-issues.yml +++ b/extensions/positron-python/.github/workflows/lock-issues.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Lock Issues' - uses: dessant/lock-threads@v4 + uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '30' diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index 4ade4fd2af1..eb8cce0bce3 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -255,7 +255,7 @@ jobs: shell: pwsh if: matrix.test-suite == 'venv' run: | - # 1. For `terminalActivation.testvirtualenvs.test.ts` + # 1. For `*.testvirtualenvs.test.ts` if ('${{ matrix.os }}' -match 'windows-latest') { $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda @@ -293,7 +293,7 @@ jobs: with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} - if: matrix.test-suite == 'venv' && matrix.os == 'ubuntu-latest' + if: matrix.test-suite == 'venv' - name: Run single-workspace tests env: @@ -451,7 +451,7 @@ jobs: PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' shell: pwsh run: | - # 1. For `terminalActivation.testvirtualenvs.test.ts` + # 1. For `*.testvirtualenvs.test.ts` if ('${{ matrix.os }}' -match 'windows-latest') { $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda diff --git a/extensions/positron-python/.gitignore b/extensions/positron-python/.gitignore index 185b9660953..18660248619 100644 --- a/extensions/positron-python/.gitignore +++ b/extensions/positron-python/.gitignore @@ -5,6 +5,7 @@ log.log **/node_modules *.pyc *.vsix +envVars.txt **/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids diff --git a/extensions/positron-python/.vscode/settings.json b/extensions/positron-python/.vscode/settings.json index eaba16a0ac4..7ba81779ca5 100644 --- a/extensions/positron-python/.vscode/settings.json +++ b/extensions/positron-python/.vscode/settings.json @@ -51,7 +51,7 @@ "prettier.printWidth": 120, "prettier.singleQuote": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.rulers": [ 100 diff --git a/extensions/positron-python/build/webpack/webpack.extension.config.js b/extensions/positron-python/build/webpack/webpack.extension.config.js index 9c1188d53fe..eae0b7b2c43 100644 --- a/extensions/positron-python/build/webpack/webpack.extension.config.js +++ b/extensions/positron-python/build/webpack/webpack.extension.config.js @@ -19,6 +19,10 @@ const config = { target: 'node', entry: { extension: './src/client/extension.ts', + 'shellExec.worker': './src/client/common/process/worker/shellExec.worker.ts', + 'plainExec.worker': './src/client/common/process/worker/plainExec.worker.ts', + 'registryKeys.worker': 'src/client/pythonEnvironments/common/registryKeys.worker.ts', + 'registryValues.worker': 'src/client/pythonEnvironments/common/registryValues.worker.ts', }, devtool: 'source-map', node: { @@ -51,6 +55,10 @@ const config = { }, ], }, + { + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, ], }, externals: [ diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 7e31ffd0933..37aee34f3a5 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -23,7 +23,8 @@ "testObserver", "quickPickItemTooltip", "saveEditor", - "terminalDataWriteEvent" + "terminalDataWriteEvent", + "terminalExecuteCommandEvent" ], "author": { "name": "Microsoft Corporation" @@ -527,6 +528,7 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", "pythonTestAdapter", "pythonREPLSmartSend", "pythonRecommendTensorboardExt" @@ -536,6 +538,7 @@ "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", "%python.experiments.pythonTestAdapter.description%", "%python.experiments.pythonREPLSmartSend.description%", "%python.experiments.pythonRecommendTensorboardExt.description%" @@ -554,6 +557,7 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", "pythonTestAdapter", "pythonREPLSmartSend" ], @@ -562,6 +566,7 @@ "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", "%python.experiments.pythonTestAdapter.description%", "%python.experiments.pythonREPLSmartSend.description%" ] @@ -690,6 +695,12 @@ "scope": "resource", "type": "array" }, + "python.REPL.enableREPLSmartSend": { + "default": true, + "description": "%python.EnableREPLSmartSend.description%", + "scope": "resource", + "type": "boolean" + }, "python.testing.autoTestDiscoverOnSaveEnabled": { "default": true, "description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%", @@ -976,6 +987,10 @@ "internalConsole" ] }, + "consoleTitle": { + "default": "Python Debug Console", + "description": "Display name of the debug console or terminal" + }, "cwd": { "default": "${workspaceFolder}", "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", @@ -1603,7 +1618,7 @@ "vscode-jsonrpc": "^8.2.0", "vscode-languageclient": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", - "vscode-tas-client": "^0.1.63", + "vscode-tas-client": "^0.1.75", "which": "^2.0.2", "winreg": "^1.2.4", "xml2js": "^0.5.0" @@ -1675,6 +1690,7 @@ "typescript": "4.5.5", "uuid": "^8.3.2", "webpack": "^5.76.0", + "worker-loader": "^3.0.8", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", "webpack-fix-default-import-plugin": "^1.0.3", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index 65437b33cef..99590eb8614 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -43,6 +43,7 @@ "python.experiments.pythonSurveyNotification.description": "Denotes the Python Survey Notification experiment.", "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", + "python.experiments.pythonDiscoveryUsingWorkers.description": "Enables use of worker threads to do heavy computation when discovering interpreters.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", "python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.", @@ -63,6 +64,7 @@ "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", + "python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", @@ -86,24 +88,76 @@ "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", "walkthrough.step.python.createPythonFolder.title": "Open a Python project folder", - "walkthrough.step.python.createPythonFile.description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", - "walkthrough.step.python.createPythonFolder.description": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", + "walkthrough.step.python.createPythonFile.description": { + "message": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "comment": [ + "{Locked='](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.createPythonFolder.description": { + "message": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", + "comment": [ + "{Locked='](command:workbench.action.files.openFolder'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.installPythonWin8.title": "Install Python", "walkthrough.step.python.installPythonWin8.description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", "walkthrough.step.python.installPythonMac.title": "Install Python", - "walkthrough.step.python.installPythonMac.description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "walkthrough.step.python.installPythonMac.description": { + "message": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "comment": [ + "{Locked='](command:python.installPythonOnMac'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.installPythonLinux.title": "Install Python", - "walkthrough.step.python.installPythonLinux.description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "walkthrough.step.python.installPythonLinux.description": { + "message": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "comment": [ + "{Locked='](command:python.installPythonOnLinux'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.selectInterpreter.title": "Select a Python Interpreter", - "walkthrough.step.python.selectInterpreter.description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", + "walkthrough.step.python.selectInterpreter.description": { + "message": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", + "comment": [ + "{Locked='](command:python.setInterpreter'}", + "{Locked='](command:workbench.action.showCommands'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.createEnvironment.title": "Create a Python Environment ", "walkthrough.step.python.createEnvironment.title2": "Create or select a Python Environment ", - "walkthrough.step.python.createEnvironment.description": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "walkthrough.step.python.createEnvironment.description": { + "message": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "comment": [ + "{Locked='](command:python.createEnvironment'}", + "{Locked='](command:workbench.action.showCommands'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.createEnvironment.description2": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", "walkthrough.step.python.learnMoreWithDS.title": "Explore more resources", - "walkthrough.step.python.learnMoreWithDS.description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "walkthrough.step.python.learnMoreWithDS.description": { + "message":"🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "comment":[ + "{Locked='](command:workbench.action.showCommands'}", + "{Locked='](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.learnMoreWithDS.description2": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", diff --git a/extensions/positron-python/pythonFiles/create_venv.py b/extensions/positron-python/pythonFiles/create_venv.py index 68f21a38b98..092286f986c 100644 --- a/extensions/positron-python/pythonFiles/create_venv.py +++ b/extensions/positron-python/pythonFiles/create_venv.py @@ -236,15 +236,15 @@ def main(argv: Optional[Sequence[str]] = None) -> None: download_pip_pyz(args.name) install_pip(args.name) - if args.toml: - print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") - install_toml(venv_path, args.extras) - requirements = get_requirements_from_args(args) if requirements: print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") install_requirements(venv_path, requirements) + if args.toml: + print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") + install_toml(venv_path, args.extras) + if __name__ == "__main__": main(sys.argv[1:]) diff --git a/extensions/positron-python/pythonFiles/deactivate.csh b/extensions/positron-python/pythonFiles/deactivate.csh deleted file mode 100644 index ef4d0d39389..00000000000 --- a/extensions/positron-python/pythonFiles/deactivate.csh +++ /dev/null @@ -1,6 +0,0 @@ -# Same as deactivate in "/bin/activate.csh" -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Initialize the variables required by deactivate function -set _OLD_VIRTUAL_PROMPT="$prompt" -set _OLD_VIRTUAL_PATH="$PATH" diff --git a/extensions/positron-python/pythonFiles/deactivate.fish b/extensions/positron-python/pythonFiles/deactivate.fish deleted file mode 100644 index c652a8c1e3d..00000000000 --- a/extensions/positron-python/pythonFiles/deactivate.fish +++ /dev/null @@ -1,36 +0,0 @@ -# Same as deactivate in "/bin/activate.fish" -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$vscode_python_old_fish_prompt_OVERRIDE" - set -e vscode_python_old_fish_prompt_OVERRIDE - if functions -q vscode_python_old_fish_prompt - functions -e fish_prompt - functions -c vscode_python_old_fish_prompt fish_prompt - functions -e vscode_python_old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - functions -e deactivate - end -end - -# Initialize the variables required by deactivate function -set -gx _OLD_VIRTUAL_PATH $PATH -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - functions -c fish_prompt vscode_python_old_fish_prompt -end -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME -end diff --git a/extensions/positron-python/pythonFiles/deactivate.ps1 b/extensions/positron-python/pythonFiles/deactivate.ps1 deleted file mode 100644 index 65dd80907d9..00000000000 --- a/extensions/positron-python/pythonFiles/deactivate.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# Same as deactivate in "Activate.ps1" -function global:deactivate ([switch]$NonDestructive) { - if (Test-Path function:_OLD_VIRTUAL_PROMPT) { - copy-item function:_OLD_VIRTUAL_PROMPT function:prompt - remove-item function:_OLD_VIRTUAL_PROMPT - } - if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) { - copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME - remove-item env:_OLD_VIRTUAL_PYTHONHOME - } - if (Test-Path env:_OLD_VIRTUAL_PATH) { - copy-item env:_OLD_VIRTUAL_PATH env:PATH - remove-item env:_OLD_VIRTUAL_PATH - } - if (Test-Path env:VIRTUAL_ENV) { - remove-item env:VIRTUAL_ENV - } - if (!$NonDestructive) { - remove-item function:deactivate - } -} - -# Initialize the variables required by deactivate function -if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { - function global:_OLD_VIRTUAL_PROMPT {""} - copy-item function:prompt function:_OLD_VIRTUAL_PROMPT -} -if (Test-Path env:PYTHONHOME) { - copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME -} -copy-item env:PATH env:_OLD_VIRTUAL_PATH diff --git a/extensions/positron-python/pythonFiles/deactivate/bash/deactivate b/extensions/positron-python/pythonFiles/deactivate/bash/deactivate new file mode 100755 index 00000000000..f6dd33425d1 --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate/bash/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +bash diff --git a/extensions/positron-python/pythonFiles/deactivate/fish/deactivate b/extensions/positron-python/pythonFiles/deactivate/fish/deactivate new file mode 100755 index 00000000000..3a9d50ccde2 --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate/fish/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +fish diff --git a/extensions/positron-python/pythonFiles/deactivate/powershell/deactivate.ps1 b/extensions/positron-python/pythonFiles/deactivate/powershell/deactivate.ps1 new file mode 100644 index 00000000000..49365e0fbef --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate/powershell/deactivate.ps1 @@ -0,0 +1,11 @@ +# Load dotenv-style file and restore environment variables +Get-Content -Path "$PSScriptRoot\envVars.txt" | ForEach-Object { + # Split each line into key and value at the first '=' + $parts = $_ -split '=', 2 + if ($parts.Count -eq 2) { + $key = $parts[0].Trim() + $value = $parts[1].Trim() + # Set the environment variable + Set-Item -Path "env:$key" -Value $value + } +} diff --git a/extensions/positron-python/pythonFiles/deactivate b/extensions/positron-python/pythonFiles/deactivate/zsh/deactivate old mode 100644 new mode 100755 similarity index 61% rename from extensions/positron-python/pythonFiles/deactivate rename to extensions/positron-python/pythonFiles/deactivate/zsh/deactivate index 6ede3da311a..8b059318f98 --- a/extensions/positron-python/pythonFiles/deactivate +++ b/extensions/positron-python/pythonFiles/deactivate/zsh/deactivate @@ -25,9 +25,20 @@ deactivate () { fi } +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) # Initialize the variables required by deactivate function -_OLD_VIRTUAL_PS1="${PS1:-}" -_OLD_VIRTUAL_PATH="$PATH" +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" fi +deactivate +zsh diff --git a/extensions/positron-python/pythonFiles/normalizeSelection.py b/extensions/positron-python/pythonFiles/normalizeSelection.py index 9afd21ca5d0..7960b5d8462 100644 --- a/extensions/positron-python/pythonFiles/normalizeSelection.py +++ b/extensions/positron-python/pythonFiles/normalizeSelection.py @@ -150,8 +150,18 @@ def traverse_file(wholeFileContent, start_line, end_line, was_highlighted): or a multiline dictionary, or differently styled multi-line list comprehension, etc. Then call the normalize_lines function to normalize our smartly selected code block. """ + parsed_file_content = None + + try: + parsed_file_content = ast.parse(wholeFileContent) + except Exception: + # Handle case where user is attempting to run code where file contains deprecated Python code. + # Let typescript side know and show warning message. + return { + "normalized_smart_result": "deprecated", + "which_line_next": 0, + } - parsed_file_content = ast.parse(wholeFileContent) smart_code = "" should_run_top_blocks = [] @@ -287,7 +297,11 @@ def get_next_block_lineno(which_line_next): data = None which_line_next = 0 - if empty_Highlight and contents.get("smartSendExperimentEnabled"): + if ( + empty_Highlight + and contents.get("smartSendExperimentEnabled") + and contents.get("smartSendSettingsEnabled") + ): result = traverse_file( contents["wholeFileContent"], vscode_start_line, @@ -296,19 +310,22 @@ def get_next_block_lineno(which_line_next): ) normalized = result["normalized_smart_result"] which_line_next = result["which_line_next"] - data = json.dumps( - # --- Start Positron --- - # Return additional info required by a Positron statement range provider. - { - "normalized": normalized, - "nextBlockLineno": result["which_line_next"], - "startLine": result["start_line"], - "endLine": result["end_line"], - "startCharacter": result["start_character"], - "endCharacter": result["end_character"], - } - # --- End Positron --- - ) + if normalized == "deprecated": + data = json.dumps({"normalized": normalized}) + else: + data = json.dumps( + # --- Start Positron --- + # Return additional info required by a Positron statement range provider. + { + "normalized": normalized, + "nextBlockLineno": result["which_line_next"], + "startLine": result["start_line"], + "endLine": result["end_line"], + "startCharacter": result["start_character"], + "endCharacter": result["end_character"], + } + # --- End Positron --- + ) else: normalized = normalize_lines(contents["code"]) data = json.dumps({"normalized": normalized}) diff --git a/extensions/positron-python/pythonFiles/printEnvVariablesToFile.py b/extensions/positron-python/pythonFiles/printEnvVariablesToFile.py index be966bcac28..a4e0d24abbe 100644 --- a/extensions/positron-python/pythonFiles/printEnvVariablesToFile.py +++ b/extensions/positron-python/pythonFiles/printEnvVariablesToFile.py @@ -2,12 +2,11 @@ # Licensed under the MIT License. import os -import json import sys +# Last argument is the target file into which we'll write the env variables line by line. +output_file = sys.argv[-1] -# Last argument is the target file into which we'll write the env variables as json. -json_file = sys.argv[-1] - -with open(json_file, "w") as outfile: - json.dump(dict(os.environ), outfile) +with open(output_file, "w") as outfile: + for key, val in os.environ.items(): + outfile.write(f"{key}={val}\n") diff --git a/extensions/positron-python/pythonFiles/pythonrc.py b/extensions/positron-python/pythonFiles/pythonrc.py new file mode 100644 index 00000000000..2b23a5c99f8 --- /dev/null +++ b/extensions/positron-python/pythonFiles/pythonrc.py @@ -0,0 +1,55 @@ +# import sys + +# original_ps1 = ">>> " + + +# class repl_hooks: +# def __init__(self): +# self.global_exit = None +# self.failure_flag = False +# self.original_excepthook = sys.excepthook +# self.original_displayhook = sys.displayhook +# sys.excepthook = self.my_excepthook +# sys.displayhook = self.my_displayhook + +# def my_displayhook(self, value): +# if value is None: +# self.failure_flag = False + +# self.original_displayhook(value) + +# def my_excepthook(self, type, value, traceback): +# self.global_exit = value +# self.failure_flag = True + +# self.original_excepthook(type, value, traceback) + + +# class ps1: +# hooks = repl_hooks() +# sys.excepthook = hooks.my_excepthook +# sys.displayhook = hooks.my_displayhook + +# # str will get called for every prompt with exit code to show success/failure +# def __str__(self): +# exit_code = 0 +# if self.hooks.failure_flag: +# exit_code = 1 +# else: +# exit_code = 0 + +# # Guide following official VS Code doc for shell integration sequence: +# # result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( +# # command_finished="\x1b]633;D;" + str(exit_code) + "\x07", +# # prompt_started="\x1b]633;A\x07", +# # prompt=original_ps1, +# # command_start="\x1b]633;B\x07", +# # command_executed="\x1b]633;C\x07", +# # ) +# result = f"{chr(27)}]633;D;{exit_code}{chr(7)}{chr(27)}]633;A{chr(7)}{original_ps1}{chr(27)}]633;B{chr(7)}{chr(27)}]633;C{chr(7)}" + +# return result + + +# if sys.platform != "win32": +# sys.ps1 = ps1() diff --git a/extensions/positron-python/pythonFiles/tests/test_create_venv.py b/extensions/positron-python/pythonFiles/tests/test_create_venv.py index 350fa5ca997..57df0a7fb3c 100644 --- a/extensions/positron-python/pythonFiles/tests/test_create_venv.py +++ b/extensions/positron-python/pythonFiles/tests/test_create_venv.py @@ -111,7 +111,7 @@ def add_gitignore(_name): ) -@pytest.mark.parametrize("install_type", ["requirements", "pyproject"]) +@pytest.mark.parametrize("install_type", ["requirements", "pyproject", "both"]) def test_install_packages(install_type): importlib.reload(create_venv) create_venv.is_installed = lambda _x: True @@ -120,16 +120,20 @@ def test_install_packages(install_type): pip_upgraded = False installing = None + order = [] + def run_process(args, error_message): - nonlocal pip_upgraded, installing + nonlocal pip_upgraded, installing, order if args[1:] == ["-m", "pip", "install", "--upgrade", "pip"]: pip_upgraded = True assert error_message == "CREATE_VENV.UPGRADE_PIP_FAILED" elif args[1:-1] == ["-m", "pip", "install", "-r"]: installing = "requirements" + order += ["requirements"] assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS" elif args[1:] == ["-m", "pip", "install", "-e", ".[test]"]: installing = "pyproject" + order += ["pyproject"] assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT" create_venv.run_process = run_process @@ -138,9 +142,23 @@ def run_process(args, error_message): create_venv.main(["--requirements", "requirements-for-test.txt"]) elif install_type == "pyproject": create_venv.main(["--toml", "pyproject.toml", "--extras", "test"]) + elif install_type == "both": + create_venv.main( + [ + "--requirements", + "requirements-for-test.txt", + "--toml", + "pyproject.toml", + "--extras", + "test", + ] + ) assert pip_upgraded - assert installing == install_type + if install_type == "both": + assert order == ["requirements", "pyproject"] + else: + assert installing == install_type @pytest.mark.parametrize( diff --git a/extensions/positron-python/pythonFiles/tests/test_shell_integration.py b/extensions/positron-python/pythonFiles/tests/test_shell_integration.py new file mode 100644 index 00000000000..98e2b1f5411 --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/test_shell_integration.py @@ -0,0 +1,48 @@ +# import importlib +# from unittest.mock import Mock + +# import pythonrc + + +# def test_decoration_success(): +# importlib.reload(pythonrc) +# ps1 = pythonrc.ps1() + +# ps1.hooks.failure_flag = False +# result = str(ps1) +# assert result == "\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" + + +# def test_decoration_failure(): +# importlib.reload(pythonrc) +# ps1 = pythonrc.ps1() + +# ps1.hooks.failure_flag = True +# result = str(ps1) + +# assert result == "\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" + + +# def test_displayhook_call(): +# importlib.reload(pythonrc) +# pythonrc.ps1() +# mock_displayhook = Mock() + +# hooks = pythonrc.repl_hooks() +# hooks.original_displayhook = mock_displayhook + +# hooks.my_displayhook("mock_value") + +# mock_displayhook.assert_called_once_with("mock_value") + + +# def test_excepthook_call(): +# importlib.reload(pythonrc) +# pythonrc.ps1() +# mock_excepthook = Mock() + +# hooks = pythonrc.repl_hooks() +# hooks.original_excepthook = mock_excepthook + +# hooks.my_excepthook("mock_type", "mock_value", "mock_traceback") +# mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback") diff --git a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py index 274fb5e5e66..e8f602a22fb 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py @@ -73,6 +73,11 @@ def discover_tests( } """ cwd = os.path.abspath(start_dir) + if "/" in start_dir: # is a subdir + parent_dir = os.path.dirname(start_dir) + sys.path.insert(0, parent_dir) + else: + sys.path.insert(0, cwd) payload: PayloadDict = {"cwd": cwd, "status": "success", "tests": None} tests = None error: List[str] = [] diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py index 8d585c35963..306b360ad70 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py @@ -64,12 +64,13 @@ def pytest_load_initial_conftests(early_config, parser, args): global TEST_UUID TEST_PORT = os.getenv("TEST_PORT") TEST_UUID = os.getenv("TEST_UUID") - error_string = ( - "PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being" - " changed or removed as they are required for successful test discovery and execution." - f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n" - ) - print(error_string, file=sys.stderr) + if TEST_UUID is None or TEST_PORT is None: + error_string = ( + "PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being" + " changed or removed as they are required for successful test discovery and execution." + f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n" + ) + print(error_string, file=sys.stderr) if "--collect-only" in args: global IS_DISCOVERY IS_DISCOVERY = True diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py index e60ee91f096..8005f3a32c0 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py @@ -64,6 +64,14 @@ try: if test_ids_from_buffer: arg_array = ["-p", "vscode_pytest"] + args + test_ids_from_buffer + print("Running pytest with args: " + str(arg_array)) + pytest.main(arg_array) + else: + print( + "Error: No test ids received from stdin, could be an error or a run request without ids provided.", + ) + print("Running pytest with no test ids as args. Args being used: ", args) + arg_array = ["-p", "vscode_pytest"] + args pytest.main(arg_array) except json.JSONDecodeError: print("Error: Could not parse test ids from stdin") diff --git a/extensions/positron-python/src/client/activation/activationManager.ts b/extensions/positron-python/src/client/activation/activationManager.ts index fac5cbeda64..763ce1ae881 100644 --- a/extensions/positron-python/src/client/activation/activationManager.ts +++ b/extensions/positron-python/src/client/activation/activationManager.ts @@ -83,6 +83,8 @@ export class ExtensionActivationManager implements IExtensionActivationManager { @traceDecoratorError('Failed to activate a workspace') public async activateWorkspace(resource: Resource): Promise { + const folder = this.workspaceService.getWorkspaceFolder(resource); + resource = folder ? folder.uri : undefined; const key = this.getWorkspaceKey(resource); if (this.activatedWorkspaces.has(key)) { return; @@ -117,8 +119,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { if (this.activatedWorkspaces.has(key)) { return; } - const folder = this.workspaceService.getWorkspaceFolder(doc.uri); - this.activateWorkspace(folder ? folder.uri : undefined).ignoreErrors(); + this.activateWorkspace(doc.uri).ignoreErrors(); } protected addHandlers(): void { diff --git a/extensions/positron-python/src/client/activation/jedi/analysisOptions.ts b/extensions/positron-python/src/client/activation/jedi/analysisOptions.ts index 845d70e839e..ab5631c4aec 100644 --- a/extensions/positron-python/src/client/activation/jedi/analysisOptions.ts +++ b/extensions/positron-python/src/client/activation/jedi/analysisOptions.ts @@ -16,6 +16,8 @@ import { ILanguageServerOutputChannel } from '../types'; export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsWithEnv { private resource: Resource | undefined; + private interpreter: PythonEnvironment | undefined; + constructor( envVarsProvider: IEnvironmentVariablesProvider, lsOutputChannel: ILanguageServerOutputChannel, @@ -28,6 +30,7 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt public async initialize(resource: Resource, interpreter: PythonEnvironment | undefined) { this.resource = resource; + this.interpreter = interpreter; return super.initialize(resource, interpreter); } @@ -85,6 +88,7 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt // --- End Positron --- workspace: { extraPaths: distinctExtraPaths, + environmentPath: this.interpreter?.envPath, symbols: { // 0 means remove limit on number of workspace symbols returned maxSymbols: 0, diff --git a/extensions/positron-python/src/client/common/application/applicationShell.ts b/extensions/positron-python/src/client/common/application/applicationShell.ts index aadf8018690..8035d979efb 100644 --- a/extensions/positron-python/src/client/common/application/applicationShell.ts +++ b/extensions/positron-python/src/client/common/application/applicationShell.ts @@ -39,7 +39,7 @@ import { WorkspaceFolderPickOptions, } from 'vscode'; import { traceError } from '../../logging'; -import { IApplicationShell, TerminalDataWriteEvent } from './types'; +import { IApplicationShell, TerminalDataWriteEvent, TerminalExecutedCommand } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -182,4 +182,12 @@ export class ApplicationShell implements IApplicationShell { return new EventEmitter().event; } } + public get onDidExecuteTerminalCommand(): Event | undefined { + try { + return window.onDidExecuteTerminalCommand; + } catch (ex) { + traceError('Failed to get proposed API TerminalExecutedCommand', ex); + return undefined; + } + } } diff --git a/extensions/positron-python/src/client/common/application/commands.ts b/extensions/positron-python/src/client/common/application/commands.ts index 8813768d35a..d0d6743d78a 100644 --- a/extensions/positron-python/src/client/common/application/commands.ts +++ b/extensions/positron-python/src/client/common/application/commands.ts @@ -72,6 +72,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ]; ['workbench.action.files.openFolder']: []; ['workbench.action.openWorkspace']: []; + ['workbench.action.openSettings']: [string]; ['setContext']: [string, boolean] | ['python.vscode.channel', Channel]; ['python.reloadVSCode']: [string]; ['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }]; diff --git a/extensions/positron-python/src/client/common/application/types.ts b/extensions/positron-python/src/client/common/application/types.ts index 863f5e4651b..6705331bf57 100644 --- a/extensions/positron-python/src/client/common/application/types.ts +++ b/extensions/positron-python/src/client/common/application/types.ts @@ -78,8 +78,42 @@ export interface TerminalDataWriteEvent { readonly data: string; } +export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; +} + export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + readonly onDidExecuteTerminalCommand: Event | undefined; /** * An [event](#Event) which fires when the focus state of the current window * changes. The value of the event represents whether the window is focused. diff --git a/extensions/positron-python/src/client/common/configSettings.ts b/extensions/positron-python/src/client/common/configSettings.ts index 5a163cc06f1..6f43bab6a1e 100644 --- a/extensions/positron-python/src/client/common/configSettings.ts +++ b/extensions/positron-python/src/client/common/configSettings.ts @@ -2,7 +2,6 @@ // eslint-disable-next-line camelcase import * as path from 'path'; -import * as fs from 'fs'; import { ConfigurationChangeEvent, ConfigurationTarget, @@ -29,14 +28,13 @@ import { IInterpreterPathService, IInterpreterSettings, IPythonSettings, + IREPLSettings, ITensorBoardSettings, ITerminalSettings, Resource, } from './types'; import { debounceSync } from './utils/decorators'; import { SystemVariables } from './variables/systemVariables'; -import { getOSType, OSType } from './utils/platform'; -import { isWindows } from './platform/platformService'; const untildify = require('untildify'); @@ -114,6 +112,8 @@ export class PythonSettings implements IPythonSettings { public globalModuleInstallation = false; + public REPL!: IREPLSettings; + public experiments!: IExperiments; // --- Start Positron --- @@ -384,7 +384,8 @@ export class PythonSettings implements IPythonSettings { activateEnvInCurrentTerminal: false, }; - const experiments = systemVariables.resolveAny(pythonSettings.get('experiments'))!; + this.REPL = pythonSettings.get('REPL')!; + const experiments = pythonSettings.get('experiments')!; if (this.experiments) { Object.assign(this.experiments, experiments); } else { @@ -411,7 +412,7 @@ export class PythonSettings implements IPythonSettings { // eslint-disable-next-line class-methods-use-this protected getPythonExecutable(pythonPath: string): string { - return getPythonExecutable(pythonPath); + return untildify(pythonPath); } protected onWorkspaceFoldersChanged(): void { @@ -510,63 +511,3 @@ function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): stri } return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); } - -function getPythonExecutable(pythonPath: string): string { - pythonPath = untildify(pythonPath) as string; - - // If only 'python'. - if ( - pythonPath === 'python' || - pythonPath.indexOf(path.sep) === -1 || - path.basename(pythonPath) === path.dirname(pythonPath) - ) { - return pythonPath; - } - - if (isValidPythonPath(pythonPath)) { - return pythonPath; - } - // Keep python right on top, for backwards compatibility. - - const KnownPythonExecutables = [ - 'python', - 'python4', - 'python3.6', - 'python3.5', - 'python3', - 'python2.7', - 'python2', - 'python3.7', - 'python3.8', - 'python3.9', - ]; - - for (let executableName of KnownPythonExecutables) { - // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (isWindows()) { - executableName = `${executableName}.exe`; - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'Scripts', executableName))) { - return path.join(pythonPath, 'Scripts', executableName); - } - } else { - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { - return path.join(pythonPath, 'bin', executableName); - } - } - } - - return pythonPath; -} - -function isValidPythonPath(pythonPath: string): boolean { - return ( - fs.existsSync(pythonPath) && - path.basename(getOSType() === OSType.Windows ? pythonPath.toLowerCase() : pythonPath).startsWith('python') - ); -} diff --git a/extensions/positron-python/src/client/common/constants.ts b/extensions/positron-python/src/client/common/constants.ts index 1f493dc436c..9ef4ccaa641 100644 --- a/extensions/positron-python/src/client/common/constants.ts +++ b/extensions/positron-python/src/client/common/constants.ts @@ -97,7 +97,8 @@ export namespace ThemeIcons { export const DEFAULT_INTERPRETER_SETTING = 'python'; -export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; +export const isCI = + process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined || process.env.GITHUB_ACTIONS === 'true'; export function isTestExecution(): boolean { return process.env.VSC_PYTHON_CI_TEST === '1' || isUnitTestExecution(); diff --git a/extensions/positron-python/src/client/common/experiments/groups.ts b/extensions/positron-python/src/client/common/experiments/groups.ts index 8f8ecc631ca..5e302c926cc 100644 --- a/extensions/positron-python/src/client/common/experiments/groups.ts +++ b/extensions/positron-python/src/client/common/experiments/groups.ts @@ -11,6 +11,10 @@ export enum TerminalEnvVarActivation { experiment = 'pythonTerminalEnvVarActivation', } +export enum DiscoveryUsingWorkers { + experiment = 'pythonDiscoveryUsingWorkers', +} + // Experiment to enable the new testing rewrite. export enum EnableTestAdapterRewrite { experiment = 'pythonTestAdapter', diff --git a/extensions/positron-python/src/client/common/process/proc.ts b/extensions/positron-python/src/client/common/process/proc.ts index 18add7daf6f..4a5aa984fa4 100644 --- a/extensions/positron-python/src/client/common/process/proc.ts +++ b/extensions/positron-python/src/client/common/process/proc.ts @@ -7,6 +7,7 @@ import { IDisposable } from '../types'; import { EnvironmentVariables } from '../variables/types'; import { execObservable, killPid, plainExec, shellExec } from './rawProcessApis'; import { ExecutionResult, IProcessService, ObservableExecutionResult, ShellOptions, SpawnOptions } from './types'; +import { workerPlainExec, workerShellExec } from './worker/rawProcessApiWrapper'; export class ProcessService extends EventEmitter implements IProcessService { private processesToKill = new Set(); @@ -47,14 +48,20 @@ export class ProcessService extends EventEmitter implements IProcessService { } public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { + this.emit('exec', file, args, options); + if (options.useWorker) { + return workerPlainExec(file, args, options); + } const execOptions = { ...options, doNotLog: true }; const promise = plainExec(file, args, execOptions, this.env, this.processesToKill); - this.emit('exec', file, args, options); return promise; } public shellExec(command: string, options: ShellOptions = {}): Promise> { this.emit('exec', command, undefined, options); + if (options.useWorker) { + return workerShellExec(command, options); + } const disposables = new Set(); const shellOptions = { ...options, doNotLog: true }; return shellExec(command, shellOptions, this.env, disposables).finally(() => { diff --git a/extensions/positron-python/src/client/common/process/rawProcessApis.ts b/extensions/positron-python/src/client/common/process/rawProcessApis.ts index 1e1b742a031..5e3641328b6 100644 --- a/extensions/positron-python/src/client/common/process/rawProcessApis.ts +++ b/extensions/positron-python/src/client/common/process/rawProcessApis.ts @@ -56,7 +56,6 @@ export function shellExec( disposables?: Set, ): Promise> { const shellOptions = getDefaultOptions(options, defaultEnv); - traceVerbose(`Shell Exec: ${command} with options: ${JSON.stringify(shellOptions, null, 4)}`); if (!options.doNotLog) { const processLogger = new ProcessLogger(new WorkspaceService()); processLogger.logProcess(command, undefined, shellOptions); @@ -75,10 +74,21 @@ export function shellExec( resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout }); } }; + let procExited = false; const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); const disposable: IDisposable = { dispose: () => { - if (!proc.killed) { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { if (proc.pid) { killPid(proc.pid); } else { @@ -114,7 +124,8 @@ export function plainExec( const deferred = createDeferred>(); const disposable: IDisposable = { dispose: () => { - if (!proc.killed) { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { if (proc.pid) { killPid(proc.pid); } else { diff --git a/extensions/positron-python/src/client/common/process/types.ts b/extensions/positron-python/src/client/common/process/types.ts index d4b742718e3..9263e69cbe2 100644 --- a/extensions/positron-python/src/client/common/process/types.ts +++ b/extensions/positron-python/src/client/common/process/types.ts @@ -26,9 +26,10 @@ export type SpawnOptions = ChildProcessSpawnOptions & { extraVariables?: NodeJS.ProcessEnv; outputChannel?: OutputChannel; stdinStr?: string; + useWorker?: boolean; }; -export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean; useWorker?: boolean }; export type ExecutionResult = { stdout: T; diff --git a/extensions/positron-python/src/client/common/process/worker/main.ts b/extensions/positron-python/src/client/common/process/worker/main.ts new file mode 100644 index 00000000000..32467361894 --- /dev/null +++ b/extensions/positron-python/src/client/common/process/worker/main.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Worker } from 'worker_threads'; +import * as path from 'path'; +import { traceVerbose, traceError } from '../../../logging/index'; + +/** + * Executes a worker file. Make sure to declare the worker file as a entry in the webpack config. + * @param workerFileName Filename of the worker file to execute, it has to end with ".worker.js" for webpack to bundle it. + * @param workerData Arguments to the worker file. + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export async function executeWorkerFile(workerFileName: string, workerData: any): Promise { + if (!workerFileName.endsWith('.worker.js')) { + throw new Error('Worker file must end with ".worker.js" for webpack to bundle webworkers'); + } + return new Promise((resolve, reject) => { + const worker = new Worker(workerFileName, { workerData }); + const id = worker.threadId; + traceVerbose( + `Worker id ${id} for file ${path.basename(workerFileName)} with data ${JSON.stringify(workerData)}`, + ); + worker.on('message', (msg: { err: Error; res: unknown }) => { + if (msg.err) { + reject(msg.err); + } + resolve(msg.res); + }); + worker.on('error', (ex: Error) => { + traceError(`Error in worker ${workerFileName}`, ex); + reject(ex); + }); + worker.on('exit', (code) => { + traceVerbose(`Worker id ${id} exited with code ${code}`); + if (code !== 0) { + reject(new Error(`Worker ${workerFileName} stopped with exit code ${code}`)); + } + }); + }); +} diff --git a/extensions/positron-python/src/client/common/process/worker/plainExec.worker.ts b/extensions/positron-python/src/client/common/process/worker/plainExec.worker.ts new file mode 100644 index 00000000000..f44ea15f965 --- /dev/null +++ b/extensions/positron-python/src/client/common/process/worker/plainExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerPlainExecImpl } from './workerRawProcessApis'; + +_workerPlainExecImpl(workerData.file, workerData.args, workerData.options) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((err) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ err }); + }); diff --git a/extensions/positron-python/src/client/common/process/worker/rawProcessApiWrapper.ts b/extensions/positron-python/src/client/common/process/worker/rawProcessApiWrapper.ts new file mode 100644 index 00000000000..e6476df5d8f --- /dev/null +++ b/extensions/positron-python/src/client/common/process/worker/rawProcessApiWrapper.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SpawnOptions } from 'child_process'; +import * as path from 'path'; +import { executeWorkerFile } from './main'; +import { ExecutionResult, ShellOptions } from './types'; + +export function workerShellExec(command: string, options: ShellOptions): Promise> { + return executeWorkerFile(path.join(__dirname, 'shellExec.worker.js'), { + command, + options, + }); +} + +export function workerPlainExec( + file: string, + args: string[], + options: SpawnOptions = {}, +): Promise> { + return executeWorkerFile(path.join(__dirname, 'plainExec.worker.js'), { + file, + args, + options, + }); +} diff --git a/extensions/positron-python/src/client/common/process/worker/shellExec.worker.ts b/extensions/positron-python/src/client/common/process/worker/shellExec.worker.ts new file mode 100644 index 00000000000..f4e9809a29a --- /dev/null +++ b/extensions/positron-python/src/client/common/process/worker/shellExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerShellExecImpl } from './workerRawProcessApis'; + +_workerShellExecImpl(workerData.command, workerData.options, workerData.defaultEnv) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((ex) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ ex }); + }); diff --git a/extensions/positron-python/src/client/common/process/worker/types.ts b/extensions/positron-python/src/client/common/process/worker/types.ts new file mode 100644 index 00000000000..5c58aec1021 --- /dev/null +++ b/extensions/positron-python/src/client/common/process/worker/types.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; + +export function noop() {} +export interface IDisposable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispose(): void | undefined | Promise; +} +export type EnvironmentVariables = Record; +export class StdErrError extends Error { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(message: string) { + super(message); + } +} + +export type SpawnOptions = ChildProcessSpawnOptions & { + encoding?: string; + // /** + // * Can't use `CancellationToken` here as it comes from vscode which is not available in worker threads. + // */ + // token?: CancellationToken; + mergeStdOutErr?: boolean; + throwOnStdErr?: boolean; + extraVariables?: NodeJS.ProcessEnv; + // /** + // * Can't use `OutputChannel` here as it comes from vscode which is not available in worker threads. + // */ + // outputChannel?: OutputChannel; + stdinStr?: string; +}; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; + +export type ExecutionResult = { + stdout: T; + stderr?: T; +}; diff --git a/extensions/positron-python/src/client/common/process/worker/workerRawProcessApis.ts b/extensions/positron-python/src/client/common/process/worker/workerRawProcessApis.ts new file mode 100644 index 00000000000..5b04aaa40b0 --- /dev/null +++ b/extensions/positron-python/src/client/common/process/worker/workerRawProcessApis.ts @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// !!!! IMPORTANT: DO NOT IMPORT FROM VSCODE MODULE AS IT IS NOT AVAILABLE INSIDE WORKER THREADS !!!! + +import { exec, execSync, spawn } from 'child_process'; +import { Readable } from 'stream'; +import { createDeferred } from '../../utils/async'; +import { DEFAULT_ENCODING } from '../constants'; +import { decodeBuffer } from '../decoder'; +import { + ShellOptions, + SpawnOptions, + EnvironmentVariables, + IDisposable, + noop, + StdErrError, + ExecutionResult, +} from './types'; + +const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; + +function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING; + const { encoding } = execOptions; + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = defaultEnv || process.env; + defaultOptions.env = { ...env }; + } else { + defaultOptions.env = { ...defaultOptions.env }; + } + + if (execOptions && execOptions.extraVariables) { + defaultOptions.env = { ...defaultOptions.env, ...execOptions.extraVariables }; + } + + // Always ensure we have unbuffered output. + defaultOptions.env.PYTHONUNBUFFERED = '1'; + if (!defaultOptions.env.PYTHONIOENCODING) { + defaultOptions.env.PYTHONIOENCODING = 'utf-8'; + } + + return defaultOptions; +} + +export function _workerShellExecImpl( + command: string, + options: ShellOptions, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const shellOptions = getDefaultOptions(options, defaultEnv); + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callback = (e: any, stdout: any, stderr: any) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + stdout = filterOutputUsingCondaRunMarkers(stdout); + // Make sure stderr is undefined if we actually had none. This is checked + // elsewhere because that's how exec behaves. + resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout }); + } + }; + let procExited = false; + const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + if (disposables) { + disposables.add(disposable); + } + }); +} + +export function _workerPlainExecImpl( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + const proc = spawn(file, args, spawnOptions); + // Listen to these errors (unhandled errors in streams tears down the process). + // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. + proc.stdout?.on('error', noop); + proc.stderr?.on('error', noop); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + disposables?.add(disposable); + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + // Tokens not supported yet as they come from vscode module which is not available. + // if (options.token) { + // internalDisposables.push(options.token.onCancellationRequested(disposable.dispose)); + // } + + const stdoutBuffers: Buffer[] = []; + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + }); + const stderrBuffers: Buffer[] = []; + on(proc.stderr, 'data', (data: Buffer) => { + if (options.mergeStdOutErr) { + stdoutBuffers.push(data); + stderrBuffers.push(data); + } else { + stderrBuffers.push(data); + } + }); + + proc.once('close', () => { + if (deferred.completed) { + return; + } + const stderr: string | undefined = + stderrBuffers.length === 0 ? undefined : decodeBuffer(stderrBuffers, encoding); + if ( + stderr && + stderr.length > 0 && + options.throwOnStdErr && + // ignore this specific error silently; see this issue for context: https://github.com/microsoft/vscode/issues/75932 + !(PS_ERROR_SCREEN_BOGUS.test(stderr) && stderr.replace(PS_ERROR_SCREEN_BOGUS, '').trim().length === 0) + ) { + deferred.reject(new StdErrError(stderr)); + } else { + let stdout = decodeBuffer(stdoutBuffers, encoding); + stdout = filterOutputUsingCondaRunMarkers(stdout); + deferred.resolve({ stdout, stderr }); + } + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + + return deferred.promise; +} + +function filterOutputUsingCondaRunMarkers(stdout: string) { + // These markers are added if conda run is used or `interpreterInfo.py` is + // run, see `get_output_via_markers.py`. + const regex = />>>PYTHON-EXEC-OUTPUT([\s\S]*)<<= 2 ? match[1].trim() : undefined; + return filteredOut !== undefined ? filteredOut : stdout; +} + +function killPid(pid: number): void { + try { + if (process.platform === 'win32') { + // Windows doesn't support SIGTERM, so execute taskkill to kill the process + execSync(`taskkill /pid ${pid} /T /F`); // NOSONAR + } else { + process.kill(pid); + } + } catch { + console.warn('Unable to kill process with pid', pid); + } +} diff --git a/extensions/positron-python/src/client/common/terminal/service.ts b/extensions/positron-python/src/client/common/terminal/service.ts index 7128d27802f..cf63603a2e9 100644 --- a/extensions/positron-python/src/client/common/terminal/service.ts +++ b/extensions/positron-python/src/client/common/terminal/service.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; +import * as path from 'path'; import { CancellationToken, Disposable, Event, EventEmitter, Terminal } from 'vscode'; import '../../common/extensions'; import { IInterpreterService } from '../../interpreter/contracts'; @@ -10,6 +11,8 @@ import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ITerminalAutoActivation } from '../../terminals/types'; import { ITerminalManager } from '../application/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; import { IConfigurationService, IDisposableRegistry } from '../types'; import { ITerminalActivator, @@ -28,6 +31,7 @@ export class TerminalService implements ITerminalService, Disposable { private terminalHelper: ITerminalHelper; private terminalActivator: ITerminalActivator; private terminalAutoActivator: ITerminalAutoActivation; + private readonly envVarScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pythonrc.py'); public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } @@ -69,14 +73,14 @@ export class TerminalService implements ITerminalService, Disposable { this.terminal!.show(preserveFocus); } } - private async ensureTerminal(preserveFocus: boolean = true): Promise { + public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; } this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); this.terminal = this.terminalManager.createTerminal({ name: this.options?.title || 'Python', - env: this.options?.env, + env: { PYTHONSTARTUP: this.envVarScript }, hideFromUser: this.options?.hideFromUser, }); this.terminalAutoActivator.disableAutoActivation(this.terminal); diff --git a/extensions/positron-python/src/client/common/terminal/shellDetector.ts b/extensions/positron-python/src/client/common/terminal/shellDetector.ts index 98cda5953fe..ad515d42c73 100644 --- a/extensions/positron-python/src/client/common/terminal/shellDetector.ts +++ b/extensions/positron-python/src/client/common/terminal/shellDetector.ts @@ -53,9 +53,6 @@ export class ShellDetector { for (const detector of shellDetectors) { shell = detector.identify(telemetryProperties, terminal); - traceVerbose( - `${detector}. Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}`, - ); if (shell && shell !== TerminalShellType.other) { telemetryProperties.failed = false; break; @@ -66,7 +63,7 @@ export class ShellDetector { // This impacts executing code in terminals and activation of environments in terminal. // So, the better this works, the better it is for the user. sendTelemetryEvent(EventName.TERMINAL_SHELL_IDENTIFICATION, undefined, telemetryProperties); - traceVerbose(`Shell identified as '${shell}'`); + traceVerbose(`Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}`); // If we could not identify the shell, use the defaults. if (shell === undefined || shell === TerminalShellType.other) { diff --git a/extensions/positron-python/src/client/common/terminal/shellDetectors/baseShellDetector.ts b/extensions/positron-python/src/client/common/terminal/shellDetectors/baseShellDetector.ts index b3793a07979..440cb8f20bb 100644 --- a/extensions/positron-python/src/client/common/terminal/shellDetectors/baseShellDetector.ts +++ b/extensions/positron-python/src/client/common/terminal/shellDetectors/baseShellDetector.ts @@ -5,7 +5,6 @@ import { injectable, unmanaged } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../../logging'; import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from '../types'; /* @@ -66,7 +65,7 @@ export abstract class BaseShellDetector implements IShellDetector { export function identifyShellFromShellPath(shellPath: string): TerminalShellType { // Remove .exe extension so shells can be more consistently detected // on Windows (including Cygwin). - const basePath = shellPath.replace(/\.exe$/, ''); + const basePath = shellPath.replace(/\.exe$/i, ''); const shell = Array.from(detectableShells.keys()).reduce((matchedShell, shellToDetect) => { if (matchedShell === TerminalShellType.other) { @@ -78,7 +77,5 @@ export function identifyShellFromShellPath(shellPath: string): TerminalShellType return matchedShell; }, TerminalShellType.other); - traceVerbose(`Shell path '${shellPath}', base path '${basePath}'`); - traceVerbose(`Shell path identified as shell '${shell}'`); return shell; } diff --git a/extensions/positron-python/src/client/common/terminal/shellDetectors/settingsShellDetector.ts b/extensions/positron-python/src/client/common/terminal/shellDetectors/settingsShellDetector.ts index 7ffc168db28..3eeb9d2e85d 100644 --- a/extensions/positron-python/src/client/common/terminal/shellDetectors/settingsShellDetector.ts +++ b/extensions/positron-python/src/client/common/terminal/shellDetectors/settingsShellDetector.ts @@ -5,7 +5,6 @@ import { inject, injectable } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../../logging'; import { IWorkspaceService } from '../../application/types'; import { IPlatformService } from '../../platform/types'; import { OSType } from '../../utils/platform'; @@ -62,7 +61,6 @@ export class SettingsShellDetector extends BaseShellDetector { } else { telemetryProperties.shellIdentificationSource = 'settings'; } - traceVerbose(`Shell path from user settings '${shellPath}'`); return shell; } } diff --git a/extensions/positron-python/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts b/extensions/positron-python/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts index 7d8ed34ebf6..bed2848ece9 100644 --- a/extensions/positron-python/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts +++ b/extensions/positron-python/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts @@ -5,7 +5,6 @@ import { inject, injectable } from 'inversify'; import { Terminal } from 'vscode'; -import { traceVerbose } from '../../../logging'; import { IPlatformService } from '../../platform/types'; import { ICurrentProcess } from '../../types'; import { OSType } from '../../utils/platform'; @@ -41,7 +40,6 @@ export class UserEnvironmentShellDetector extends BaseShellDetector { if (shell !== TerminalShellType.other) { telemetryProperties.shellIdentificationSource = 'environment'; } - traceVerbose(`Shell path from user env '${shellPath}'`); return shell; } } diff --git a/extensions/positron-python/src/client/common/types.ts b/extensions/positron-python/src/client/common/types.ts index 09aef5e4707..feab482b2e0 100644 --- a/extensions/positron-python/src/client/common/types.ts +++ b/extensions/positron-python/src/client/common/types.ts @@ -188,6 +188,7 @@ export interface IPythonSettings { // --- End Positron --- readonly defaultInterpreterPath: string; readonly tensorBoard: ITensorBoardSettings | undefined; + readonly REPL: IREPLSettings; register(): void; } @@ -207,6 +208,10 @@ export interface ITerminalSettings { readonly activateEnvInCurrentTerminal: boolean; } +export interface IREPLSettings { + readonly enableREPLSmartSend: boolean; +} + export interface IExperiments { /** * Return `true` if experiments are enabled, else `false`. diff --git a/extensions/positron-python/src/client/common/utils/async.ts b/extensions/positron-python/src/client/common/utils/async.ts index 5905399cd4a..c119d8f19b0 100644 --- a/extensions/positron-python/src/client/common/utils/async.ts +++ b/extensions/positron-python/src/client/common/utils/async.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-async-promise-executor */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -228,3 +230,35 @@ export async function flattenIterator(iterator: IAsyncIterator): Promise Promise} condition + * @param {number} timeoutMs + * @param {string} errorMessage + * @returns {Promise} + */ +export async function waitForCondition( + condition: () => Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout); + + clearTimeout(timer); + reject(new Error(errorMessage)); + }, timeoutMs); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { + return; + } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); + }); +} diff --git a/extensions/positron-python/src/client/common/utils/decorators.ts b/extensions/positron-python/src/client/common/utils/decorators.ts index 689eb9acad4..44a82ee1376 100644 --- a/extensions/positron-python/src/client/common/utils/decorators.ts +++ b/extensions/positron-python/src/client/common/utils/decorators.ts @@ -1,5 +1,5 @@ import '../../common/extensions'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError } from '../../logging'; import { isTestExecution } from '../constants'; import { createDeferred, Deferred } from './async'; import { getCacheKeyFromFunctionArgs, getGlobalCacheStore } from './cacheUtils'; @@ -161,7 +161,6 @@ export function cache(expiryDurationMs: number, cachePromise = false, expiryDura } const cachedItem = cacheStoreForMethods.get(key); if (cachedItem && (cachedItem.expiry > Date.now() || expiryDurationMs === -1)) { - traceVerbose(`Cached data exists ${key}`); return Promise.resolve(cachedItem.data); } const expiryMs = diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index 95252b361c7..ae4d7efa38a 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -4,6 +4,7 @@ 'use strict'; import { l10n } from 'vscode'; +import { Commands } from '../constants'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -42,6 +43,9 @@ export namespace Diagnostics { export const pylanceDefaultMessage = l10n.t( "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); + export const invalidSmartSendMessage = l10n.t(`Python is unable to parse the code provided. Please + turn off Smart Send if you wish to always run line by line or explicitly select code + to force run. See [logs](command:${Commands.ViewOutput}) for more details`); } export namespace Common { @@ -92,6 +96,9 @@ export namespace AttachProcess { export const refreshList = l10n.t('Refresh process list'); } +export namespace Repl { + export const disableSmartSend = l10n.t('Disable Smart Send'); +} export namespace Pylance { export const remindMeLater = l10n.t('Remind me later'); @@ -197,7 +204,7 @@ export namespace Interpreters { export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); export const terminalEnvVarCollectionPrompt = l10n.t( - '{0} environment was successfully activated, even though {1} may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + '{0} environment was successfully activated, even though {1} indicator may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); export const terminalDeactivateProgress = l10n.t('Editing {0}...'); export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); diff --git a/extensions/positron-python/src/client/common/variables/systemVariables.ts b/extensions/positron-python/src/client/common/variables/systemVariables.ts index eb318b2f491..05e5d9d6f58 100644 --- a/extensions/positron-python/src/client/common/variables/systemVariables.ts +++ b/extensions/positron-python/src/client/common/variables/systemVariables.ts @@ -132,6 +132,8 @@ export class SystemVariables extends AbstractSystemVariables { const basename = Path.basename(folder.uri.fsPath); ((this as any) as Record)[`workspaceFolder:${basename}`] = folder.uri.fsPath; + ((this as any) as Record)[`workspaceFolder:${folder.name}`] = + folder.uri.fsPath; }); } catch { // This try...catch block is here to support pre-existing tests, ignore error. diff --git a/extensions/positron-python/src/client/interpreter/activation/service.ts b/extensions/positron-python/src/client/interpreter/activation/service.ts index f97545a5823..586bad0d765 100644 --- a/extensions/positron-python/src/client/interpreter/activation/service.ts +++ b/extensions/positron-python/src/client/interpreter/activation/service.ts @@ -259,7 +259,9 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi shellInfo.shellType, interpreter, ); - traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); + traceVerbose( + `Activation Commands received ${activationCommands} for shell ${shellInfo.shell}, resource ${resource?.fsPath} and interpreter ${interpreter?.path}`, + ); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { if (interpreter && [EnvironmentType.Venv, EnvironmentType.Pyenv].includes(interpreter?.envType)) { const key = getSearchPathEnvVarNames()[0]; @@ -273,15 +275,15 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } return undefined; } - // Run the activate command collect the environment from it. - const activationCommand = fixActivationCommands(activationCommands).join(' && '); - // In order to make sure we know where the environment output is, - // put in a dummy echo we can look for const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes( shellInfo.shellType, ) ? ';' : '&&'; + // Run the activate command collect the environment from it. + const activationCommand = fixActivationCommands(activationCommands).join(` ${commandSeparator} `); + // In order to make sure we know where the environment output is, + // put in a dummy echo we can look for command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join( ' ', )}`; @@ -292,7 +294,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi const oldWarnings = env[PYTHON_WARNINGS]; env[PYTHON_WARNINGS] = 'ignore'; - traceVerbose(`${hasCustomEnvVars ? 'Has' : 'No'} Custom Env Vars`); traceVerbose(`Activating Environment to capture Environment variables, ${command}`); // Do some wrapping of the call. For two reasons: diff --git a/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts index b4dcfe36e09..d5470e528ab 100644 --- a/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts +++ b/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -91,12 +91,9 @@ export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { } if (process.env.VSCODE_CLI !== '1') { // We only want to select the interpreter if VS Code was launched from the command line. - traceVerbose( - 'VS Code was not launched from the command line, not selecting activated interpreter', - JSON.stringify(process.env, undefined, 4), - ); return undefined; } + traceVerbose('VS Code was not launched from the command line'); const prefix = await this.getPrefixOfSelectedActivatedEnv(); if (!prefix) { this._promptIfApplicable().ignoreErrors(); diff --git a/extensions/positron-python/src/client/positron/statementRange.ts b/extensions/positron-python/src/client/positron/statementRange.ts index 3ac4f0bdd99..8e2956abe88 100644 --- a/extensions/positron-python/src/client/positron/statementRange.ts +++ b/extensions/positron-python/src/client/positron/statementRange.ts @@ -119,6 +119,7 @@ async function provideStatementRangeFromAst( // Hardcode these to true so that smart send is enabled in the script. emptyHighlight: true, smartSendExperimentEnabled: true, + smartSendSettingsEnabled: true, }); observable.proc?.stdin?.write(input); observable.proc?.stdin?.end(); @@ -126,6 +127,13 @@ async function provideStatementRangeFromAst( const outputRaw = await outputPromise.promise; const output = JSON.parse(outputRaw); + // Unfortunately, the script handles code with a syntax error by returning 'deprecated', and by + // only returning the `normalized` key. We use that information to distinguish from the user + // trying to actually execute the code 'deprecated' (e.g. if it's a variable in their script). + if (Object.keys(output).length === 1 && output.normalized === 'deprecated') { + throw new Error('Failed to parse the Python script.'); + } + return { // parse() doesn't do anything at the time of writing this, but we call it on // object.normalized anyway since that's how it's used upstream. diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index fb1a791d07e..cc37b1f82cf 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -88,14 +88,13 @@ export class EnvsCollectionService extends PythonEnvsWatcher { traceError(`Failed to resolve ${path}`, ex); return undefined; }); - traceVerbose(`Resolved ${path} to ${JSON.stringify(resolved)}`); + traceVerbose(`Resolved ${path} using downstream locator`); if (resolved) { this.cache.addEnv(resolved, true); } 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 0cca49e2b4c..fd45b4909af 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 @@ -109,7 +109,7 @@ async function updateEnvUsingRegistry(env: PythonEnvInfo): Promise { let interpreters = getRegistryInterpretersSync(); if (!interpreters) { traceError('Expected registry interpreter cache to be initialized already'); - interpreters = await getRegistryInterpreters(); + interpreters = await getRegistryInterpreters(true); } const data = interpreters.find((i) => arePathsSame(i.interpreterPath, env.executable.filename)); if (data) { diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts index 7cac0cb7df9..651a43ff886 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts @@ -6,6 +6,8 @@ import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; import { Conda, getCondaEnvironmentsTxt } from '../../../common/environmentManagers/conda'; import { traceError, traceVerbose } from '../../../../logging'; import { FSWatchingLocator } from './fsWatchingLocator'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { inExperiment } from '../../../common/externalDependencies'; export class CondaEnvironmentLocator extends FSWatchingLocator { public readonly providerId: string = 'conda-envs'; @@ -19,8 +21,11 @@ export class CondaEnvironmentLocator extends FSWatchingLocator { } // eslint-disable-next-line class-methods-use-this - public async *doIterEnvs(): IPythonEnvsIterator { - const conda = await Conda.getConda(); + public async *doIterEnvs( + _: unknown, + useWorkerThreads = inExperiment(DiscoveryUsingWorkers.experiment), + ): IPythonEnvsIterator { + const conda = await Conda.getConda(undefined, useWorkerThreads); if (conda === undefined) { traceVerbose(`Couldn't locate the conda binary.`); return; diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts index 5bfc62d99d4..b2f5123069b 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -63,7 +63,14 @@ export class WindowsPathEnvVarLocator implements ILocator, IDispos // Note that we do no filtering here, including to check if files // are valid executables. That is left to callers (e.g. composite // locators). - return this.locators.iterEnvs(query); + async function* iterator(it: IPythonEnvsIterator) { + traceVerbose(`Searching windows known paths locator`); + for await (const env of it) { + yield env; + } + traceVerbose(`Finished searching windows known paths locator`); + } + return iterator(this.locators.iterEnvs(query)); } } @@ -93,11 +100,7 @@ function getDirFilesLocator( // rather than in each low-level locator. In the meantime we // take a naive approach. async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { - traceVerbose('Searching for windows path interpreters'); - yield* await getEnvs(locator.iterEnvs(query)).then((res) => { - traceVerbose('Finished searching for windows path interpreters'); - return res; - }); + yield* await getEnvs(locator.iterEnvs(query)).then((res) => res); } return { providerId: locator.providerId, 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 b52e9c35779..16b2167021d 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 @@ -7,15 +7,20 @@ import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator'; import { getRegistryInterpreters } from '../../../common/windowsUtils'; import { traceError, traceVerbose } from '../../../../logging'; import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; +import { inExperiment } from '../../../common/externalDependencies'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; export class WindowsRegistryLocator extends Locator { public readonly providerId: string = 'windows-registry'; // eslint-disable-next-line class-methods-use-this - public iterEnvs(): IPythonEnvsIterator { + public iterEnvs( + _?: unknown, + useWorkerThreads = inExperiment(DiscoveryUsingWorkers.experiment), + ): IPythonEnvsIterator { const iterator = async function* () { traceVerbose('Searching for windows registry interpreters'); - const interpreters = await getRegistryInterpreters(); + const interpreters = await getRegistryInterpreters(useWorkerThreads); for (const interpreter of interpreters) { try { // Filter out Microsoft Store app directories. We have a store app locator that handles this. 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 8f048ddd067..bc98e57829e 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -265,16 +265,21 @@ export class Conda { * @param command - Command used to spawn conda. This has the same meaning as the * first argument of spawn() - i.e. it can be a full path, or just a binary name. */ - constructor(readonly command: string, shellCommand?: string, private readonly shellPath?: string) { + constructor( + readonly command: string, + shellCommand?: string, + private readonly shellPath?: string, + private readonly useWorkerThreads = true, + ) { this.shellCommand = shellCommand ?? command; onDidChangePythonSetting(CONDAPATH_SETTING_KEY, () => { Conda.condaPromise = new Map>(); }); } - public static async getConda(shellPath?: string): Promise { + public static async getConda(shellPath?: string, useWorkerThreads?: boolean): Promise { if (Conda.condaPromise.get(shellPath) === undefined || isTestExecution()) { - Conda.condaPromise.set(shellPath, Conda.locate(shellPath)); + Conda.condaPromise.set(shellPath, Conda.locate(shellPath, useWorkerThreads)); } return Conda.condaPromise.get(shellPath); } @@ -285,10 +290,15 @@ export class Conda { * * @return A Conda instance corresponding to the binary, if successful; otherwise, undefined. */ - private static async locate(shellPath?: string): Promise { + private static async locate(shellPath?: string, useWorkerThreads = true): Promise { traceVerbose(`Searching for conda.`); const home = getUserHomeDir(); - const customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + let customCondaPath: string | undefined = 'conda'; + try { + customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + } catch (ex) { + traceError(`Failed to get conda path setting, ${ex}`); + } const suffix = getOSType() === OSType.Windows ? 'Scripts\\conda.exe' : 'bin/conda'; // Produce a list of candidate binaries to be probed by exec'ing them. @@ -307,7 +317,7 @@ export class Conda { } async function* getCandidatesFromRegistry() { - const interps = await getRegistryInterpreters(); + const interps = await getRegistryInterpreters(useWorkerThreads); const candidates = interps .filter((interp) => interp.interpreterPath && interp.distroOrgName === 'ContinuumAnalytics') .map((interp) => path.join(path.win32.dirname(interp.interpreterPath), suffix)); @@ -385,7 +395,7 @@ export class Conda { // Probe the candidates, and pick the first one that exists and does what we need. for await (const condaPath of getCandidates()) { traceVerbose(`Probing conda binary: ${condaPath}`); - let conda = new Conda(condaPath, undefined, shellPath); + let conda = new Conda(condaPath, undefined, shellPath, useWorkerThreads); try { await conda.getInfo(); if (getOSType() === OSType.Windows && (isTestExecution() || condaPath !== customCondaPath)) { @@ -440,7 +450,7 @@ export class Conda { if (shellPath) { options.shell = shellPath; } - const resultPromise = exec(command, ['info', '--json'], options); + const resultPromise = exec(command, ['info', '--json'], options, this.useWorkerThreads); // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. // Hence explicitly continue execution after timeout has been reached. const success = await Promise.race([ diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts index 80a185dc299..d8b1b2ff649 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { getEnvironmentVariable } from '../../../common/utils/platform'; -import { traceError } from '../../../logging'; +import { traceError, traceVerbose } from '../../../logging'; import { arePathsSame, normCasePath, pathExists, readFile } from '../externalDependencies'; function getSearchHeight() { @@ -85,7 +85,7 @@ async function getProjectDir(envFolder: string): Promise { } const projectDir = (await readFile(dotProjectFile)).trim(); if (!(await pathExists(projectDir))) { - traceError( + traceVerbose( `The .project file inside environment folder: ${envFolder} doesn't contain a valid path to the project`, ); return undefined; diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts b/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts index b64d47f4226..2ecbbdf0cb1 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts @@ -6,11 +6,12 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types'; -import { IDisposable, IConfigurationService } from '../../common/types'; +import { IDisposable, IConfigurationService, IExperimentService } from '../../common/types'; import { chain, iterable } from '../../common/utils/async'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; import { traceError, traceVerbose } from '../../logging'; +import { DiscoveryUsingWorkers } from '../../common/experiments/groups'; let internalServiceContainer: IServiceContainer; export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { @@ -20,15 +21,28 @@ export function initializeExternalDependencies(serviceContainer: IServiceContain // processes export async function shellExecute(command: string, options: ShellOptions = {}): Promise> { + const useWorker = inExperiment(DiscoveryUsingWorkers.experiment); const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; return service.shellExec(command, options); } -export async function exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { +export async function exec( + file: string, + args: string[], + options: SpawnOptions = {}, + useWorker = inExperiment(DiscoveryUsingWorkers.experiment), +): Promise> { const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; return service.exec(file, args, options); } +export function inExperiment(experimentName: string): boolean { + const service = internalServiceContainer.get(IExperimentService); + return service.inExperimentSync(experimentName); +} + // Workspace export function isVirtualWorkspace(): boolean { diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/registryKeys.worker.ts b/extensions/positron-python/src/client/pythonEnvironments/common/registryKeys.worker.ts new file mode 100644 index 00000000000..05996d057f1 --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/common/registryKeys.worker.ts @@ -0,0 +1,24 @@ +import { Registry } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryKey } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryKeys(keys: IRegistryKey[]): IRegistryKey[] { + // Use the map function to create a new array with copies of the specified properties. + return keys.map((key) => ({ + hive: key.hive, + arch: key.arch, + key: key.key, + })); +} + +regKey.keys((err: Error, res: Registry[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryKeys(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/registryValues.worker.ts b/extensions/positron-python/src/client/pythonEnvironments/common/registryValues.worker.ts new file mode 100644 index 00000000000..eaef7cbd58a --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/common/registryValues.worker.ts @@ -0,0 +1,27 @@ +import { RegistryItem } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryValue } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryValues(values: IRegistryValue[]): IRegistryValue[] { + // Use the map function to create a new array with copies of the specified properties. + return values.map((value) => ({ + hive: value.hive, + arch: value.arch, + key: value.key, + name: value.name, + type: value.type, + value: value.value, + })); +} + +regKey.values((err: Error, res: RegistryItem[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryValues(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/windowsRegistry.ts b/extensions/positron-python/src/client/pythonEnvironments/common/windowsRegistry.ts index 30047e1c990..801ef0c907b 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/windowsRegistry.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/windowsRegistry.ts @@ -1,8 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { HKCU, HKLM, Options, REG_SZ, Registry, RegistryItem } from 'winreg'; +import * as path from 'path'; import { createDeferred } from '../../common/utils/async'; +import { executeWorkerFile } from '../../common/process/worker/main'; export { HKCU, HKLM, REG_SZ, Options }; @@ -22,30 +25,36 @@ export interface IRegistryValue { value: string; } -export async function readRegistryValues(options: Options): Promise { - // eslint-disable-next-line global-require - const WinReg = require('winreg'); - const regKey = new WinReg(options); - const deferred = createDeferred(); - regKey.values((err: Error, res: RegistryItem[]) => { - if (err) { - deferred.reject(err); - } - deferred.resolve(res); - }); - return deferred.promise; +export async function readRegistryValues(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.values((err: Error, res: RegistryItem[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryValues.worker.js'), options); } -export async function readRegistryKeys(options: Options): Promise { - // eslint-disable-next-line global-require - const WinReg = require('winreg'); - const regKey = new WinReg(options); - const deferred = createDeferred(); - regKey.keys((err: Error, res: Registry[]) => { - if (err) { - deferred.reject(err); - } - deferred.resolve(res); - }); - return deferred.promise; +export async function readRegistryKeys(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.keys((err: Error, res: Registry[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryKeys.worker.js'), options); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/windowsUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/common/windowsUtils.ts index a47025ceef6..e9210371be0 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/windowsUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/windowsUtils.ts @@ -54,13 +54,14 @@ export interface IRegistryInterpreterData { async function getInterpreterDataFromKey( { arch, hive, key }: IRegistryKey, distroOrgName: string, + useWorkerThreads: boolean, ): Promise { const result: IRegistryInterpreterData = { interpreterPath: '', distroOrgName, }; - const values: IRegistryValue[] = await readRegistryValues({ arch, hive, key }); + const values: IRegistryValue[] = await readRegistryValues({ arch, hive, key }, useWorkerThreads); for (const value of values) { switch (value.name) { case 'SysArchitecture': @@ -80,10 +81,10 @@ async function getInterpreterDataFromKey( } } - const subKeys: IRegistryKey[] = await readRegistryKeys({ arch, hive, key }); + const subKeys: IRegistryKey[] = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); const subKey = subKeys.map((s) => s.key).find((s) => s.endsWith('InstallPath')); if (subKey) { - const subKeyValues: IRegistryValue[] = await readRegistryValues({ arch, hive, key: subKey }); + const subKeyValues: IRegistryValue[] = await readRegistryValues({ arch, hive, key: subKey }, useWorkerThreads); const value = subKeyValues.find((v) => v.name === 'ExecutablePath'); if (value) { result.interpreterPath = value.value; @@ -103,10 +104,13 @@ export async function getInterpreterDataFromRegistry( arch: string, hive: string, key: string, + useWorkerThreads: boolean, ): Promise { - const subKeys = await readRegistryKeys({ arch, hive, key }); + const subKeys = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); const distroOrgName = key.substr(key.lastIndexOf('\\') + 1); - const allData = await Promise.all(subKeys.map((subKey) => getInterpreterDataFromKey(subKey, distroOrgName))); + const allData = await Promise.all( + subKeys.map((subKey) => getInterpreterDataFromKey(subKey, distroOrgName, useWorkerThreads)), + ); return (allData.filter((data) => data !== undefined) || []) as IRegistryInterpreterData[]; } @@ -122,15 +126,15 @@ export function getRegistryInterpretersSync(): IRegistryInterpreterData[] | unde let registryInterpretersPromise: Promise | undefined; -export async function getRegistryInterpreters(): Promise { +export async function getRegistryInterpreters(useWorkerThreads: boolean): Promise { if (!isTestExecution() && registryInterpretersPromise !== undefined) { return registryInterpretersPromise; } - registryInterpretersPromise = getRegistryInterpretersImpl(); + registryInterpretersPromise = getRegistryInterpretersImpl(useWorkerThreads); return registryInterpretersPromise; } -async function getRegistryInterpretersImpl(): Promise { +async function getRegistryInterpretersImpl(useWorkerThreads: boolean): Promise { let registryData: IRegistryInterpreterData[] = []; for (const arch of ['x64', 'x86']) { @@ -138,13 +142,15 @@ async function getRegistryInterpretersImpl(): Promise k.key); + keys = (await readRegistryKeys({ arch, hive, key: root }, useWorkerThreads)).map((k) => k.key); } catch (ex) { traceError(`Failed to access Registry: ${arch}\\${hive}\\${root}`, ex); } for (const key of keys) { - registryData = registryData.concat(await getInterpreterDataFromRegistry(arch, hive, key)); + registryData = registryData.concat( + await getInterpreterDataFromRegistry(arch, hive, key, useWorkerThreads), + ); } } } diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 51c55e41424..617a2996801 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -18,7 +18,7 @@ import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils' import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; import { deleteCondaEnvironment } from './condaDeleteUtils'; -const RECOMMENDED_CONDA_PYTHON = '3.10'; +const RECOMMENDED_CONDA_PYTHON = '3.11'; export async function getCondaBaseEnv(): Promise { const conda = await Conda.getConda(); @@ -47,7 +47,7 @@ export async function getCondaBaseEnv(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8'].map((v) => ({ + const items: QuickPickItem[] = ['3.11', '3.12', '3.10', '3.9', '3.8'].map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', description: v, })); diff --git a/extensions/positron-python/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/extensions/positron-python/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index c70cd896b22..67b877429a2 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -36,6 +36,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi disposableRegistry, platformService, interpreterService, + commandManager, ); this.terminalTitle = 'Django Shell'; disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); diff --git a/extensions/positron-python/src/client/terminals/codeExecution/helper.ts b/extensions/positron-python/src/client/terminals/codeExecution/helper.ts index 058c78e332a..48a435c8710 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/helper.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/helper.ts @@ -6,6 +6,7 @@ import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; import { + IActiveResourceService, IApplicationShell, ICommandManager, IDocumentManager, @@ -36,6 +37,8 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly commandManager: ICommandManager; + private activeResourceService: IActiveResourceService; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read. private readonly configSettings: IConfigurationService; @@ -47,6 +50,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { this.interpreterService = serviceContainer.get(IInterpreterService); this.configSettings = serviceContainer.get(IConfigurationService); this.commandManager = serviceContainer.get(ICommandManager); + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); } public async normalizeLines(code: string, wholeFileContent?: string, resource?: Uri): Promise { @@ -90,6 +94,13 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const endLineVal = activeEditor?.selection?.end.line ?? 0; const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; const smartSendExperimentEnabledVal = pythonSmartSendEnabled(this.serviceContainer); + let smartSendSettingsEnabledVal = false; + const configuration = this.serviceContainer.get(IConfigurationService); + if (configuration) { + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend; + } + const input = JSON.stringify({ code, wholeFileContent, @@ -97,6 +108,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { endLine: endLineVal, emptyHighlight: emptyHighlightVal, smartSendExperimentEnabled: smartSendExperimentEnabledVal, + smartSendSettingsEnabled: smartSendSettingsEnabledVal, }); observable.proc?.stdin?.write(input); observable.proc?.stdin?.end(); @@ -105,7 +117,12 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const result = await normalizeOutput.promise; const object = JSON.parse(result); - if (activeEditor?.selection) { + if ( + activeEditor?.selection && + smartSendExperimentEnabledVal && + smartSendSettingsEnabledVal && + object.normalized !== 'deprecated' + ) { const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; await this.moveToNextBlock(lineOffset, activeEditor); } diff --git a/extensions/positron-python/src/client/terminals/codeExecution/repl.ts b/extensions/positron-python/src/client/terminals/codeExecution/repl.ts index e3a4bc7582c..45f19798c3d 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/repl.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/repl.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { Disposable } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { ICommandManager, IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; @@ -21,6 +21,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider { @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IPlatformService) platformService: IPlatformService, @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(ICommandManager) commandManager: ICommandManager, ) { super( terminalServiceFactory, @@ -29,6 +30,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider { disposableRegistry, platformService, interpreterService, + commandManager, ); this.terminalTitle = 'REPL'; } diff --git a/extensions/positron-python/src/client/terminals/codeExecution/terminalCodeExecution.ts b/extensions/positron-python/src/client/terminals/codeExecution/terminalCodeExecution.ts index 50270c3586c..a257fff20db 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -6,15 +6,17 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { ICommandManager, IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { Diagnostics, Repl } from '../../common/utils/localize'; +import { showWarningMessage } from '../../common/vscodeApis/windowApis'; import { IInterpreterService } from '../../interpreter/contracts'; +import { traceInfo } from '../../logging'; import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { ICodeExecutionService } from '../../terminals/types'; - @injectable() export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; @@ -27,6 +29,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IDisposableRegistry) protected readonly disposables: Disposable[], @inject(IPlatformService) protected readonly platformService: IPlatformService, @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, + @inject(ICommandManager) protected readonly commandManager: ICommandManager, ) {} public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { @@ -42,9 +45,17 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { if (!code || code.trim().length === 0) { return; } - await this.initializeRepl(resource); - await this.getTerminalService(resource).sendText(code); + if (code == 'deprecated') { + // If user is trying to smart send deprecated code show warning + const selection = await showWarningMessage(Diagnostics.invalidSmartSendMessage, Repl.disableSmartSend); + traceInfo(`Selected file contains invalid Python or Deprecated Python 2 code`); + if (selection === Repl.disableSmartSend) { + this.configurationService.updateSetting('REPL.enableREPLSmartSend', false, resource); + } + } else { + await this.getTerminalService(resource).sendText(code); + } } public async initializeRepl(resource: Resource) { const terminalService = this.getTerminalService(resource); diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts deleted file mode 100644 index a9fd804291a..00000000000 --- a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { - Position, - Uri, - WorkspaceEdit, - Range, - TextEditorRevealType, - ProgressLocation, - Terminal, - Selection, -} from 'vscode'; -import { - IApplicationEnvironment, - IApplicationShell, - IDocumentManager, - ITerminalManager, -} from '../../common/application/types'; -import { IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; -import { Common, Interpreters } from '../../common/utils/localize'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; -import { TerminalShellType } from '../../common/terminal/types'; -import { traceError } from '../../logging'; -import { shellExec } from '../../common/process/rawProcessApis'; -import { sleep } from '../../common/utils/async'; -import { getDeactivateShellInfo } from './deactivateScripts'; -import { isTestExecution } from '../../common/constants'; -import { ProgressService } from '../../common/application/progressService'; -import { copyFile, createFile, pathExists } from '../../common/platform/fs-paths'; -import { getOSType, OSType } from '../../common/utils/platform'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; -@injectable() -export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private terminalProcessId: number | undefined; - - private readonly progressService: ProgressService; - - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, - @inject(IExperimentService) private readonly experimentService: IExperimentService, - ) { - this.progressService = new ProgressService(this.appShell); - } - - public async activate(): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - return; - } - if (!isTestExecution()) { - // Avoid showing prompt until startup completes. - await sleep(6000); - } - this.disposableRegistry.push( - this.appShell.onDidWriteTerminalData(async (e) => { - if (!e.data.includes('deactivate')) { - return; - } - let shellType = identifyShellFromShellPath(this.appEnvironment.shell); - if (shellType === TerminalShellType.commandPrompt) { - return; - } - if (getOSType() === OSType.OSX && shellType === TerminalShellType.bash) { - // On macOS, sometimes bash is overriden by OS to actually launch zsh, so we need to execute inside - // the shell to get the correct shell type. - const shell = await shellExec('echo $SHELL', { shell: this.appEnvironment.shell }).then((output) => - output.stdout.trim(), - ); - shellType = identifyShellFromShellPath(shell); - } - const { terminal } = e; - const cwd = - 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd - ? terminal.creationOptions.cwd - : undefined; - const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.type !== PythonEnvType.Virtual) { - return; - } - await this._notifyUsers(shellType, terminal).catch((ex) => traceError('Deactivate prompt failed', ex)); - }), - ); - } - - public async _notifyUsers(shellType: TerminalShellType, terminal: Terminal): Promise { - const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( - `${terminalDeactivationPromptKey}-${shellType}`, - true, - ); - if (!notificationPromptEnabled.value) { - const processId = await terminal.processId; - if (processId && this.terminalProcessId === processId) { - // Existing terminal needs to be restarted for changes to take effect. - await this.forceRestartShell(terminal); - } - return; - } - const scriptInfo = getDeactivateShellInfo(shellType); - if (!scriptInfo) { - // Shell integration is not supported for these shells, in which case this workaround won't work. - return; - } - const telemetrySelections: ['Edit script', "Don't show again"] = ['Edit script', "Don't show again"]; - const { initScript, source, destination } = scriptInfo; - const prompts = [Common.editSomething.format(initScript.displayName), Common.doNotShowAgain]; - const selection = await this.appShell.showWarningMessage( - Interpreters.terminalDeactivatePrompt.format(initScript.displayName), - ...prompts, - ); - let index = selection ? prompts.indexOf(selection) : 0; - if (selection === prompts[0]) { - index = 0; - } - sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { - selection: selection ? telemetrySelections[index] : undefined, - }); - if (!selection) { - return; - } - if (selection === prompts[0]) { - this.progressService.showProgress({ - location: ProgressLocation.Window, - title: Interpreters.terminalDeactivateProgress.format(initScript.displayName), - }); - await copyFile(source, destination); - await this.openScriptWithEdits(initScript.command, initScript.contents); - await notificationPromptEnabled.updateValue(false); - this.progressService.hideProgress(); - this.terminalProcessId = await terminal.processId; - } - if (selection === prompts[1]) { - await notificationPromptEnabled.updateValue(false); - } - } - - private async openScriptWithEdits(command: string, content: string) { - const document = await this.openScript(command); - const hookMarker = 'VSCode venv deactivate hook'; - content = ` -# >>> ${hookMarker} >>> -${content} -# <<< ${hookMarker} <<<`; - // If script already has the hook, don't add it again. - const editor = await this.documentManager.showTextDocument(document); - if (document.getText().includes(hookMarker)) { - editor.revealRange( - new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), - TextEditorRevealType.AtTop, - ); - return; - } - const editorEdit = new WorkspaceEdit(); - editorEdit.insert(document.uri, new Position(document.lineCount, 0), content); - await this.documentManager.applyEdit(editorEdit); - // Reveal the edits. - editor.selection = new Selection(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)); - editor.revealRange( - new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), - TextEditorRevealType.AtTop, - ); - } - - private async openScript(command: string) { - const initScriptPath = await this.getPathToScript(command); - if (!(await pathExists(initScriptPath))) { - await createFile(initScriptPath); - } - const document = await this.documentManager.openTextDocument(initScriptPath); - return document; - } - - private async getPathToScript(command: string) { - return shellExec(command, { shell: this.appEnvironment.shell }).then((output) => output.stdout.trim()); - } - - public async forceRestartShell(terminal: Terminal): Promise { - terminal.dispose(); - terminal = this.terminalManager.createTerminal({ - message: Interpreters.restartingTerminal, - }); - terminal.show(true); - terminal.sendText('deactivate'); - } -} diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts deleted file mode 100644 index 34917e44bbd..00000000000 --- a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable no-case-declarations */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; -import { TerminalShellType } from '../../common/terminal/types'; - -type DeactivateShellInfo = { - /** - * Full path to source deactivate script to copy. - */ - source: string; - /** - * Full path to destination to copy deactivate script to. - */ - destination: string; - initScript: { - /** - * Display name of init script for the shell. - */ - displayName: string; - /** - * Command to run in shell to output the full path to init script. - */ - command: string; - /** - * Contents to add to init script. - */ - contents: string; - }; -}; - -// eslint-disable-next-line global-require -const untildify: (value: string) => string = require('untildify'); - -export function getDeactivateShellInfo(shellType: TerminalShellType): DeactivateShellInfo | undefined { - switch (shellType) { - case TerminalShellType.bash: - return buildInfo( - 'deactivate', - { - displayName: '~/.bashrc', - path: '~/.bashrc', - }, - `source {0}`, - ); - case TerminalShellType.powershellCore: - case TerminalShellType.powershell: - return buildInfo( - 'deactivate.ps1', - { - displayName: 'Powershell Profile', - path: '$Profile', - }, - `& "{0}"`, - ); - case TerminalShellType.zsh: - return buildInfo( - 'deactivate', - { - displayName: '~/.zshrc', - path: '~/.zshrc', - }, - `source {0}`, - ); - case TerminalShellType.fish: - return buildInfo( - 'deactivate.fish', - { - displayName: 'config.fish', - path: '$__fish_config_dir/config.fish', - }, - `source {0}`, - ); - case TerminalShellType.cshell: - return buildInfo( - 'deactivate.csh', - { - displayName: '~/.cshrc', - path: '~/.cshrc', - }, - `source {0}`, - ); - default: - return undefined; - } -} - -function buildInfo( - deactivate: string, - initScript: { - path: string; - displayName: string; - }, - scriptCommandFormat: string, -) { - const scriptPath = path.join('~', '.vscode-python', deactivate); - return { - source: path.join(_SCRIPTS_DIR, deactivate), - destination: untildify(scriptPath), - initScript: { - displayName: initScript.displayName, - command: `echo ${initScript.path}`, - contents: scriptCommandFormat.format(scriptPath), - }, - }; -} diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateService.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateService.ts new file mode 100644 index 00000000000..0758f3e2231 --- /dev/null +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateService.ts @@ -0,0 +1,102 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ITerminalManager } from '../../common/application/types'; +import { pathExists } from '../../common/platform/fs-paths'; +import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; +import { Resource } from '../../common/types'; +import { waitForCondition } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceVerbose } from '../../logging'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalDeactivateService } from '../types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +@injectable() +export class TerminalDeactivateService implements ITerminalDeactivateService { + private readonly envVarScript = path.join(_SCRIPTS_DIR, 'printEnvVariablesToFile.py'); + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(ITerminalHelper) private readonly terminalHelper: ITerminalHelper, + ) {} + + @cache(-1, true) + public async initializeScriptParams(shell: string): Promise { + const location = this.getLocation(shell); + if (!location) { + return; + } + const shellType = identifyShellFromShellPath(shell); + const terminal = this.terminalManager.createTerminal({ + name: `Python ${shellType} Deactivate`, + shellPath: shell, + hideFromUser: true, + cwd: location, + }); + const globalInterpreters = this.interpreterService.getInterpreters().filter((i) => !i.type); + const outputFile = path.join(location, `envVars.txt`); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + const checkIfFileHasBeenCreated = () => pathExists(outputFile); + const stopWatch = new StopWatch(); + const command = this.terminalHelper.buildCommandForTerminal(shellType, interpreterPath, [ + this.envVarScript, + outputFile, + ]); + terminal.sendText(command); + await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); + traceVerbose(`Time taken to get env vars using terminal is ${stopWatch.elapsedTime}ms`); + } + + public async getScriptLocation(shell: string, resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return undefined; + } + return this.getLocation(shell); + } + + private getLocation(shell: string) { + const shellType = identifyShellFromShellPath(shell); + if (!ShellIntegrationShells.includes(shellType)) { + return undefined; + } + return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType)); + } + + private getShellFolderName(shellType: TerminalShellType): string { + switch (shellType) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.fish: + return 'fish'; + case TerminalShellType.zsh: + return 'zsh'; + case TerminalShellType.bash: + return 'bash'; + default: + throw new Error(`Unsupported shell type ${shellType}`); + } + } +} diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts index 46e70d60d92..b9a9fc17b3d 100644 --- a/extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts @@ -37,8 +37,7 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { ITerminalEnvVarCollectionService } from '../types'; -import { ShellIntegrationShells } from './shellIntegration'; +import { IShellIntegrationService, ITerminalDeactivateService, ITerminalEnvVarCollectionService } from '../types'; import { ProgressService } from '../../common/application/progressService'; @injectable() @@ -79,7 +78,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalDeactivateService) private readonly terminalDeactivateService: ITerminalDeactivateService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IShellIntegrationService) private readonly shellIntegrationService: IShellIntegrationService, ) { this.separator = platform.osType === OSType.Windows ? ';' : ':'; this.progressService = new ProgressService(this.shell); @@ -121,7 +122,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.disposables, ); const { shell } = this.applicationEnvironment; - const isActive = this.isShellIntegrationActive(shell); + const isActive = await this.shellIntegrationService.isWorking(shell); const shellType = identifyShellFromShellPath(shell); if (!isActive && shellType !== TerminalShellType.commandPrompt) { traceWarn(`Shell integration is not active, environment activated maybe overriden by the shell.`); @@ -139,7 +140,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ location: ProgressLocation.Window, title: Interpreters.activatingTerminals, }); - await this._applyCollectionImpl(resource, shell); + await this._applyCollectionImpl(resource, shell).catch((ex) => { + traceError(`Failed to apply terminal env vars`, shell, ex); + return Promise.reject(ex); // Ensures progress indicator does not disappear in case of errors, so we can catch issues faster. + }); this.progressService.hideProgress(); } @@ -184,10 +188,11 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); - const prependOptions = this.getPrependOptions(shell); + const prependOptions = await this.getPrependOptions(shell); // Clear any previously set env vars from collection envVarCollection.clear(); + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; @@ -206,14 +211,19 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { // Prefer prepending to PATH instead of replacing it, as we do not want to replace any // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) - const prependedPart = env.PATH.slice(0, -processEnv.PATH.length); - value = prependedPart; + value = env.PATH.slice(0, -processEnv.PATH.length); + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); envVarCollection.prepend(key, value, prependOptions); } else { if (!value.endsWith(this.separator)) { value = value.concat(this.separator); } + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); envVarCollection.prepend(key, value, prependOptions); } @@ -233,6 +243,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.description = description; await this.trackTerminalPrompt(shell, resource, env); + await this.terminalDeactivateService.initializeScriptParams(shell).catch((ex) => { + traceError(`Failed to initialize deactivate script`, shell, ex); + }); } private isPromptSet = new Map(); @@ -277,7 +290,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 should be set but no PS1 was set. return; } - const config = this.isShellIntegrationActive(shell); + const config = await this.shellIntegrationService.isWorking(shell); if (!config) { traceVerbose('PS1 is not set when shell integration is disabled.'); return; @@ -332,8 +345,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } - private getPrependOptions(shell: string): EnvironmentVariableMutatorOptions { - const isActive = this.isShellIntegrationActive(shell); + private async getPrependOptions(shell: string): Promise { + const isActive = await this.shellIntegrationService.isWorking(shell); // Ideally we would want to prepend exactly once, either at shell integration or process creation. // TODO: Stop prepending altogether once https://github.com/microsoft/vscode/issues/145234 is available. return isActive @@ -347,21 +360,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ }; } - private isShellIntegrationActive(shell: string): boolean { - const isEnabled = this.workspaceService - .getConfiguration('terminal') - .get('integrated.shellIntegration.enabled')!; - if (isEnabled && ShellIntegrationShells.includes(identifyShellFromShellPath(shell))) { - // Unfortunately shell integration could still've failed in remote scenarios, we can't know for sure: - // https://code.visualstudio.com/docs/terminal/shell-integration#_automatic-script-injection - return true; - } - if (!isEnabled) { - traceVerbose('Shell integrated is disabled in user settings.'); - } - return false; - } - private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; return envVarCollection.getScoped(scope); @@ -406,6 +404,8 @@ function shouldSkip(env: string) { 'SHLVL', // Even though this maybe returned, setting it can result in output encoding errors in terminal. 'PYTHONUTF8', + // We have deactivate service which takes care of setting it. + '_OLD_VIRTUAL_PATH', ].includes(env); } diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts deleted file mode 100644 index 1be2501595a..00000000000 --- a/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TerminalShellType } from '../../common/terminal/types'; - -/** - * This is a list of shells which support shell integration: - * https://code.visualstudio.com/docs/terminal/shell-integration - */ -export const ShellIntegrationShells = [ - TerminalShellType.powershell, - TerminalShellType.powershellCore, - TerminalShellType.bash, - TerminalShellType.zsh, - TerminalShellType.fish, -]; diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegrationService.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegrationService.ts new file mode 100644 index 00000000000..485af6f3cc9 --- /dev/null +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegrationService.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable, inject } from 'inversify'; +import { IApplicationShell, ITerminalManager, IWorkspaceService } from '../../common/application/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { createDeferred, sleep } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { IShellIntegrationService } from '../types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +@injectable() +export class ShellIntegrationService implements IShellIntegrationService { + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + ) {} + + public async isWorking(shell: string): Promise { + return this._isWorking(shell).catch((ex) => { + traceError(`Failed to determine if shell supports shell integration`, shell, ex); + return false; + }); + } + + @cache(-1, true) + public async _isWorking(shell: string): Promise { + const isEnabled = this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled')!; + if (!isEnabled) { + traceVerbose('Shell integrated is disabled in user settings.'); + } + const shellType = identifyShellFromShellPath(shell); + const isSupposedToWork = isEnabled && ShellIntegrationShells.includes(shellType); + if (!isSupposedToWork) { + return false; + } + const deferred = createDeferred(); + const timestamp = new Date().getTime(); + const name = `Python ${timestamp}`; + const onDidExecuteTerminalCommand = this.appShell.onDidExecuteTerminalCommand?.bind(this.appShell); + if (!onDidExecuteTerminalCommand) { + // Proposed API is not available, assume shell integration is working at this point. + return true; + } + try { + const disposable = onDidExecuteTerminalCommand((e) => { + if (e.terminal.name === name) { + deferred.resolve(); + } + }); + const terminal = this.terminalManager.createTerminal({ + name, + shellPath: shell, + hideFromUser: true, + }); + terminal.sendText(`echo ${shell}`); + const success = await Promise.race([sleep(3000).then(() => false), deferred.promise.then(() => true)]); + disposable.dispose(); + if (!success) { + traceInfo(`Shell integration is not working for ${shellType}`); + } + return success; + } catch (ex) { + traceVerbose(`Proposed API is not available, failed to subscribe to onDidExecuteTerminalCommand`, ex); + // Proposed API is not available, assume shell integration is working at this point. + return true; + } + } +} diff --git a/extensions/positron-python/src/client/terminals/serviceRegistry.ts b/extensions/positron-python/src/client/terminals/serviceRegistry.ts index a9da776d011..3474edadd74 100644 --- a/extensions/positron-python/src/client/terminals/serviceRegistry.ts +++ b/extensions/positron-python/src/client/terminals/serviceRegistry.ts @@ -12,13 +12,16 @@ import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, + IShellIntegrationService, ITerminalAutoActivation, + ITerminalDeactivateService, ITerminalEnvVarCollectionService, } from './types'; import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; -import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; +import { ShellIntegrationService } from './envCollectionActivation/shellIntegrationService'; +import { TerminalDeactivateService } from './envCollectionActivation/deactivateService'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); @@ -42,13 +45,11 @@ export function registerTypes(serviceManager: IServiceManager): void { ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService, ); + serviceManager.addSingleton(ITerminalDeactivateService, TerminalDeactivateService); serviceManager.addSingleton( IExtensionSingleActivationService, TerminalIndicatorPrompt, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalDeactivateLimitationPrompt, - ); + serviceManager.addSingleton(IShellIntegrationService, ShellIntegrationService); serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/extensions/positron-python/src/client/terminals/types.ts b/extensions/positron-python/src/client/terminals/types.ts index ba30b8f6d47..0fb268fe192 100644 --- a/extensions/positron-python/src/client/terminals/types.ts +++ b/extensions/positron-python/src/client/terminals/types.ts @@ -41,3 +41,14 @@ export interface ITerminalEnvVarCollectionService { */ isTerminalPromptSetCorrectly(resource?: Resource): boolean; } + +export const IShellIntegrationService = Symbol('IShellIntegrationService'); +export interface IShellIntegrationService { + isWorking(shell: string): Promise; +} + +export const ITerminalDeactivateService = Symbol('ITerminalDeactivateService'); +export interface ITerminalDeactivateService { + initializeScriptParams(shell: string): Promise; + getScriptLocation(shell: string, resource: Resource): Promise; +} diff --git a/extensions/positron-python/src/test/activation/activationManager.unit.test.ts b/extensions/positron-python/src/test/activation/activationManager.unit.test.ts index 2b8d54f12ee..6ee2572214b 100644 --- a/extensions/positron-python/src/test/activation/activationManager.unit.test.ts +++ b/extensions/positron-python/src/test/activation/activationManager.unit.test.ts @@ -82,6 +82,12 @@ suite('Activation Manager', () => { test('If running in a virtual workspace, do not activate services that do not support it', async () => { when(workspaceService.isVirtualWorkspace).thenReturn(true); const resource = Uri.parse('two'); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -112,6 +118,12 @@ suite('Activation Manager', () => { test('If running in a untrusted workspace, do not activate services that do not support it', async () => { when(workspaceService.isTrusted).thenReturn(false); const resource = Uri.parse('two'); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -150,6 +162,13 @@ suite('Activation Manager', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + await managerTest.activateWorkspace(resource); autoSelection.verifyAll(); @@ -289,6 +308,12 @@ suite('Activation Manager', () => { .setup((a) => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); + const workspaceFolder = { + index: 0, + name: 'one', + uri: resource, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); await managerTest.activateWorkspace(resource); await managerTest.activateWorkspace(resource); diff --git a/extensions/positron-python/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts b/extensions/positron-python/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts index 8104ed2730b..39ae0cfb21a 100644 --- a/extensions/positron-python/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts +++ b/extensions/positron-python/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts @@ -12,6 +12,8 @@ import { WorkspaceService } from '../../../client/common/application/workspace'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { IConfigurationService } from '../../../client/common/types'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { Architecture } from '../../../client/common/utils/platform'; suite('Jedi LSP - analysis Options', () => { const workspacePath = path.join('this', 'is', 'fake', 'workspace', 'path'); @@ -74,6 +76,25 @@ suite('Jedi LSP - analysis Options', () => { expect(result.initializationOptions.workspace.symbols.maxSymbols).to.deep.equal(0); }); + test('With interpreter path', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(configurationService.getSettings(anything())).thenReturn({} as any); + const pythonEnvironment: PythonEnvironment = { + envPath: '.../.venv', + id: 'base_env', + envType: EnvironmentType.Conda, + path: '.../.venv/bin/python', + architecture: Architecture.x86, + sysPrefix: 'prefix/path', + }; + analysisOptions.initialize(undefined, pythonEnvironment); + + const result = await analysisOptions.getAnalysisOptions(); + + expect(result.initializationOptions.workspace.environmentPath).to.deep.equal('.../.venv'); + }); + test('Without extraPaths provided and no workspace', async () => { when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/extensions/positron-python/src/test/common/experiments/service.unit.test.ts b/extensions/positron-python/src/test/common/experiments/service.unit.test.ts index ab05db6da5a..00aba3fc1ea 100644 --- a/extensions/positron-python/src/test/common/experiments/service.unit.test.ts +++ b/extensions/positron-python/src/test/common/experiments/service.unit.test.ts @@ -8,7 +8,9 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; import { Disposable } from 'vscode-jsonrpc'; -import * as tasClient from 'vscode-tas-client'; +// sinon can not create a stub if we just point to the exported module +import * as tasClient from 'vscode-tas-client/vscode-tas-client/VSCodeTasClient'; +import * as expService from 'vscode-tas-client'; import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; @@ -180,7 +182,7 @@ suite('Experimentation service', () => { getTreatmentVariable = sinon.stub().returns(true); sinon.stub(tasClient, 'getExperimentationService').returns(({ getTreatmentVariable, - } as unknown) as tasClient.IExperimentationService); + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); }); @@ -218,7 +220,7 @@ suite('Experimentation service', () => { getTreatmentVariable = sinon.stub().returns(false); sinon.stub(tasClient, 'getExperimentationService').returns(({ getTreatmentVariable, - } as unknown) as tasClient.IExperimentationService); + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); @@ -364,7 +366,7 @@ suite('Experimentation service', () => { getTreatmentVariableStub = sinon.stub().returns(Promise.resolve('value')); sinon.stub(tasClient, 'getExperimentationService').returns(({ getTreatmentVariable: getTreatmentVariableStub, - } as unknown) as tasClient.IExperimentationService); + } as unknown) as expService.IExperimentationService); configureApplicationEnvironment('stable', extensionVersion); }); diff --git a/extensions/positron-python/src/test/common/process/proc.exec.test.ts b/extensions/positron-python/src/test/common/process/proc.exec.test.ts index c193df95d08..7e771e884b8 100644 --- a/extensions/positron-python/src/test/common/process/proc.exec.test.ts +++ b/extensions/positron-python/src/test/common/process/proc.exec.test.ts @@ -34,6 +34,16 @@ suite('ProcessService Observable', () => { expect(result.stderr).to.equal(undefined, 'stderr not undefined'); }); + test('When using worker threads, exec should output print statements', async () => { + const procService = new ProcessService(); + const printOutput = '1234'; + const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`], { useWorker: true }); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + test('exec should output print unicode characters', async function () { // This test has not been working for many months in Python 2.7 under // Windows. Tracked by #2546. (unicode under Py2.7 is tough!) @@ -241,6 +251,18 @@ suite('ProcessService Observable', () => { expect(result.stderr).to.equal(undefined, 'stderr not empty'); expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); }); + test('When using worker threads, shellExec should be able to run python and filter output using conda related markers', async () => { + const procService = new ProcessService(); + const printOutput = '1234'; + const result = await procService.shellExec( + `"${pythonPath}" -c "print('>>>PYTHON-EXEC-OUTPUT');print('${printOutput}');print('<< { const procService = new ProcessService(); const result = procService.shellExec('invalid command'); diff --git a/extensions/positron-python/src/test/common/terminals/service.unit.test.ts b/extensions/positron-python/src/test/common/terminals/service.unit.test.ts index 2f0d86f4000..7336f7094e6 100644 --- a/extensions/positron-python/src/test/common/terminals/service.unit.test.ts +++ b/extensions/positron-python/src/test/common/terminals/service.unit.test.ts @@ -2,9 +2,11 @@ // Licensed under the MIT License. import { expect } from 'chai'; +import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { Disposable, Terminal as VSCodeTerminal, WorkspaceConfiguration } from 'vscode'; import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; @@ -158,6 +160,37 @@ suite('Terminal Service', () => { terminal.verify((t) => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); }); + test('Ensure PYTHONSTARTUP is injected', async () => { + service = new TerminalService(mockServiceContainer.object); + terminalActivator + .setup((h) => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + terminalManager + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + const envVarScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pythonrc.py'); + terminalManager + .setup((t) => + t.createTerminal({ + name: TypeMoq.It.isAny(), + env: TypeMoq.It.isObjectWith({ PYTHONSTARTUP: envVarScript }), + hideFromUser: TypeMoq.It.isAny(), + }), + ) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + await service.show(); + await service.show(); + await service.show(); + await service.show(); + + terminalHelper.verifyAll(); + terminalActivator.verifyAll(); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + }); + test('Ensure terminal is activated once after creation', async () => { service = new TerminalService(mockServiceContainer.object); terminalActivator diff --git a/extensions/positron-python/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts b/extensions/positron-python/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts index 07befdda929..e58e455ea7e 100644 --- a/extensions/positron-python/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts +++ b/extensions/positron-python/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts @@ -41,6 +41,7 @@ suite('Shell Detectors', () => { shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.ksh); shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); shellPathsAndIdentification.set('c:\\windows\\system32\\pwsh.exe', TerminalShellType.powershellCore); + shellPathsAndIdentification.set('C:\\Program Files\\nu\\bin\\nu.EXE', TerminalShellType.nushell); shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/pwsh', TerminalShellType.powershellCore); shellPathsAndIdentification.set('/usr/bin/fish', TerminalShellType.fish); diff --git a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 88b9c978854..c1955c2704e 100644 --- a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -12,7 +12,6 @@ import { GlobalEnvironmentVariableCollection, ProgressLocation, Uri, - WorkspaceConfiguration, WorkspaceFolder, } from 'vscode'; import { @@ -38,6 +37,7 @@ import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { IShellIntegrationService, ITerminalDeactivateService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -50,29 +50,32 @@ suite('Terminal Environment Variable Collection Service', () => { let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; - let workspaceConfig: WorkspaceConfiguration; let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; + let terminalDeactivateService: ITerminalDeactivateService; const progressOptions = { location: ProgressLocation.Window, title: Interpreters.activatingTerminals, }; let configService: IConfigurationService; + let shellIntegrationService: IShellIntegrationService; const displayPath = 'display/path'; const customShell = 'powershell'; const defaultShell = defaultShells[getOSType()]; setup(() => { workspaceService = mock(); - workspaceConfig = mock(); + terminalDeactivateService = mock(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve(undefined); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); when(workspaceService.workspaceFolders).thenReturn(undefined); - when(workspaceService.getConfiguration('terminal')).thenReturn(instance(workspaceConfig)); - when(workspaceConfig.get('integrated.shellIntegration.enabled')).thenReturn(true); platform = mock(); when(platform.osType).thenReturn(getOSType()); interpreterService = mock(); context = mock(); shell = mock(); + shellIntegrationService = mock(); + when(shellIntegrationService.isWorking(anything())).thenResolve(true); globalCollection = mock(); collection = mock(); when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); @@ -107,7 +110,9 @@ suite('Terminal Environment Variable Collection Service', () => { instance(environmentActivationService), instance(workspaceService), instance(configService), + instance(terminalDeactivateService), new PathUtils(getOSType() === OSType.Windows), + instance(shellIntegrationService), ); }); @@ -334,6 +339,42 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Also prepend deactivate script location if available', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenReject(); // Verify we swallow errors from here + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + verify(collection.prepend('PATH', `scriptLocation${separator}${prependedPart}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Prepend full PATH with separator otherwise', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); @@ -367,6 +408,42 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Prepend full PATH with separator otherwise', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `scriptLocation${separator}${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Verify envs are not applied if env activation is disabled', async () => { const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( @@ -445,8 +522,8 @@ suite('Terminal Environment Variable Collection Service', () => { }); test('Correct track that prompt was set for PS1 if shell integration is disabled', async () => { - reset(workspaceConfig); - when(workspaceConfig.get('integrated.shellIntegration.enabled')).thenReturn(false); + reset(shellIntegrationService); + when(shellIntegrationService.isWorking(anything())).thenResolve(false); when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; const ps1Shell = 'bash'; diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts index 1b2ea8715d3..b3e1084a56b 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -10,9 +11,14 @@ import { CondaEnvironmentLocator } from '../../../../../client/pythonEnvironment import { sleep } from '../../../../core'; import { createDeferred, Deferred } from '../../../../../client/common/utils/async'; import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; -import { TEST_TIMEOUT } from '../../../../constants'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../../constants'; import { traceWarn } from '../../../../../client/logging'; import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { getEnvs } from '../../common'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../../ciConstants'; +import { isCI } from '../../../../../client/common/constants'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; class CondaEnvs { private readonly condaEnvironmentsTxt; @@ -50,9 +56,13 @@ class CondaEnvs { } } -suite('Conda Env Watcher', async () => { +suite('Conda Env Locator', async () => { let locator: CondaEnvironmentLocator; let condaEnvsTxt: CondaEnvs; + const envsLocation = + PYTHON_VIRTUAL_ENVS_LOCATION !== undefined + ? path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) + : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); async function waitForChangeToBeDetected(deferred: Deferred) { const timeout = setTimeout(() => { @@ -61,11 +71,21 @@ suite('Conda Env Watcher', async () => { }, TEST_TIMEOUT); await deferred.promise; } + let envPaths: any; + + suiteSetup(async () => { + if (isCI) { + envPaths = await fs.readJson(envsLocation); + } + }); setup(async () => { sinon.stub(platformUtils, 'getUserHomeDir').returns(TEST_LAYOUT_ROOT); condaEnvsTxt = new CondaEnvs(); await condaEnvsTxt.cleanUp(); + if (isCI) { + sinon.stub(externalDependencies, 'getPythonSetting').returns(envPaths.condaExecPath); + } }); async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise) { @@ -111,4 +131,14 @@ suite('Conda Env Watcher', async () => { assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); }); + + test('Worker thread to fetch conda environments is working', async () => { + locator = new CondaEnvironmentLocator(); + const items = await getEnvs(locator.doIterEnvs(undefined, false)); + const workerItems = await getEnvs(locator.doIterEnvs(undefined, true)); + console.log('Number of items Conda locator returned:', items.length); + // Make sure items returned when using worker threads v/s not are the same. + assertBasicEnvsEqual(items, workerItems); + assert(workerItems.length > 0, 'No environments found'); + }).timeout(TEST_TIMEOUT * 2); }); diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts index 276f28cd665..1bf3ef19398 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts @@ -17,14 +17,14 @@ suite('Conda Python Version Parser Tests', () => { setup(() => { readFileStub = sinon.stub(externalDeps, 'readFile'); + sinon.stub(externalDeps, 'inExperiment').returns(false); pathExistsStub = sinon.stub(externalDeps, 'pathExists'); pathExistsStub.resolves(true); }); teardown(() => { - readFileStub.restore(); - pathExistsStub.restore(); + sinon.restore(); }); interface ICondaPythonVersionTestData { diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.testvirtualenvs.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.testvirtualenvs.ts new file mode 100644 index 00000000000..693d7c1b7fe --- /dev/null +++ b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.testvirtualenvs.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { WindowsRegistryLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { TEST_TIMEOUT } from '../../../../constants'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; + +suite('Windows Registry Locator', async () => { + let locator: WindowsRegistryLocator; + + setup(function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + locator = new WindowsRegistryLocator(); + return undefined; + }); + + test('Worker thread to fetch registry interpreters is working', async () => { + const items = await getEnvs(locator.iterEnvs(undefined, false)); + const workerItems = await getEnvs(locator.iterEnvs(undefined, true)); + console.log('Number of items Windows registry locator returned:', items.length); + // Make sure items returned when using worker threads v/s not are the same. + assertBasicEnvsEqual(items, workerItems); + }).timeout(TEST_TIMEOUT * 2); +}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts index c4621b267ad..a69a643f52d 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts @@ -11,6 +11,7 @@ import { WindowsRegistryLocator } from '../../../../../client/pythonEnvironments import { createBasicEnv } from '../../common'; import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; import { assertBasicEnvsEqual } from '../envTestUtils'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; suite('Windows Registry', () => { let stubReadRegistryValues: sinon.SinonStub; @@ -200,6 +201,7 @@ suite('Windows Registry', () => { } setup(async () => { + sinon.stub(externalDependencies, 'inExperiment').returns(false); stubReadRegistryValues = sinon.stub(winreg, 'readRegistryValues'); stubReadRegistryKeys = sinon.stub(winreg, 'readRegistryKeys'); stubReadRegistryValues.callsFake(fakeRegistryValues); diff --git a/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index 1e9de68ad77..268c5333a42 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -593,6 +593,11 @@ suite('Conda and its environments are located correctly', () => { }, }, }; + sinon.stub(externalDependencies, 'inExperiment').returns(false); + }); + + teardown(() => { + sinon.restore(); }); test('Must compute conda environment name from prefix', async () => { diff --git a/extensions/positron-python/src/test/pythonFiles/terminalExec/sample_invalid_smart_selection.py b/extensions/positron-python/src/test/pythonFiles/terminalExec/sample_invalid_smart_selection.py new file mode 100644 index 00000000000..73d9e0fba06 --- /dev/null +++ b/extensions/positron-python/src/test/pythonFiles/terminalExec/sample_invalid_smart_selection.py @@ -0,0 +1,10 @@ +def beliebig(x, y, *mehr): + print "x=", x, ", x=", y + print "mehr: ", mehr + +list = [ +1, +2, +3, +] +print("Above is invalid");print("deprecated");print("show warning") diff --git a/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts b/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts index 8d70ab6e01e..01f490e2b25 100644 --- a/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts +++ b/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts @@ -1,22 +1,28 @@ import * as TypeMoq from 'typemoq'; import * as path from 'path'; -import { TextEditor, Selection, Position, TextDocument } from 'vscode'; +import { TextEditor, Selection, Position, TextDocument, Uri } from 'vscode'; import * as fs from 'fs-extra'; import { SemVer } from 'semver'; import { assert, expect } from 'chai'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, +} from '../../../client/common/application/types'; import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { IConfigurationService, IExperimentService } from '../../../client/common/types'; +import { IConfigurationService, IExperimentService, IPythonSettings } from '../../../client/common/types'; import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; import { IServiceContainer } from '../../../client/ioc/types'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; import { EnableREPLSmartSend } from '../../../client/common/experiments/groups'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { Commands, EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { PYTHON_PATH } from '../../common'; import { Architecture } from '../../../client/common/utils/platform'; import { ProcessService } from '../../../client/common/process/proc'; +import { l10n } from '../../mocks/vsc'; const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); @@ -35,8 +41,11 @@ suite('REPL - Smart Send', () => { let experimentService: TypeMoq.IMock; let processService: TypeMoq.IMock; + let activeResourceService: TypeMoq.IMock; let document: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + const workingPython: PythonEnvironment = { path: PYTHON_PATH, version: new SemVer('3.6.6-final'), @@ -64,7 +73,9 @@ suite('REPL - Smart Send', () => { serviceContainer = TypeMoq.Mock.ofType(); experimentService = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); - + activeResourceService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + const resource = Uri.parse('a'); // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((x: any) => x.then).returns(() => undefined); serviceContainer @@ -92,6 +103,14 @@ suite('REPL - Smart Send', () => { interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(workingPython)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + + pythonSettings.setup((s) => s.REPL).returns(() => ({ enableREPLSmartSend: true, REPLSmartSend: true })); + + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); document = TypeMoq.Mock.ofType(); @@ -149,6 +168,16 @@ suite('REPL - Smart Send', () => { .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) .returns(() => true); + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: true, + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const activeEditor = TypeMoq.Mock.ofType(); const firstIndexPosition = new Position(0, 0); const selection = TypeMoq.Mock.ofType(); @@ -226,4 +255,58 @@ suite('REPL - Smart Send', () => { const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); }); + + test('Smart Send should provide warning when code is not valid', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: true, + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile( + path.join(TEST_FILES_PATH, `sample_invalid_smart_selection.py`), + 'utf8', + ); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + applicationShell + .setup((a) => + a.showWarningMessage( + l10n.t( + `Python is unable to parse the code provided. Please + turn off Smart Send if you wish to always run line by line or explicitly select code + to force run. [logs](command:${Commands.ViewOutput}) for more details.`, + ), + 'Switch to line-by-line', + ), + ) + .verifiable(TypeMoq.Times.once()); + }); }); diff --git a/extensions/positron-python/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/extensions/positron-python/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index 9523f35a104..93845e6189e 100644 --- a/extensions/positron-python/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/extensions/positron-python/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -84,6 +84,7 @@ suite('Terminal - Code Execution', () => { disposables, platform.object, interpreterService.object, + commandManager.object, ); break; } @@ -95,6 +96,7 @@ suite('Terminal - Code Execution', () => { disposables, platform.object, interpreterService.object, + commandManager.object, ); expectedTerminalTitle = 'REPL'; break; diff --git a/extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts deleted file mode 100644 index f775241abb3..00000000000 --- a/extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, TerminalDataWriteEvent, TextDocument, TextEditor, Uri } from 'vscode'; -import * as sinon from 'sinon'; -import { expect } from 'chai'; -import { - IApplicationEnvironment, - IApplicationShell, - IDocumentManager, - ITerminalManager, -} from '../../../client/common/application/types'; -import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { Common, Interpreters } from '../../../client/common/utils/localize'; -import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; -import { sleep } from '../../core'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; -import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; -import { TerminalShellType } from '../../../client/common/terminal/types'; -import * as processApi from '../../../client/common/process/rawProcessApis'; -import * as fsapi from '../../../client/common/platform/fs-paths'; -import { noop } from '../../../client/common/utils/misc'; - -suite('Terminal Deactivation Limitation Prompt', () => { - let shell: IApplicationShell; - let experimentService: IExperimentService; - let persistentStateFactory: IPersistentStateFactory; - let appEnvironment: IApplicationEnvironment; - let deactivatePrompt: TerminalDeactivateLimitationPrompt; - let terminalWriteEvent: EventEmitter; - let notificationEnabled: IPersistentState; - let interpreterService: IInterpreterService; - let terminalManager: ITerminalManager; - let documentManager: IDocumentManager; - const prompts = [Common.editSomething.format('~/.bashrc'), Common.doNotShowAgain]; - const expectedMessage = Interpreters.terminalDeactivatePrompt.format('~/.bashrc'); - const initScriptPath = 'home/node/.bashrc'; - const resource = Uri.file('a'); - let terminal: Terminal; - - setup(async () => { - const activeEditorEvent = new EventEmitter(); - const document = ({ - uri: Uri.file(''), - getText: () => '', - } as unknown) as TextDocument; - sinon.stub(processApi, 'shellExec').callsFake(async (command: string) => { - if (command !== 'echo ~/.bashrc') { - throw new Error(`Unexpected command: ${command}`); - } - await sleep(1500); - return { stdout: initScriptPath }; - }); - documentManager = mock(); - terminalManager = mock(); - terminal = ({ - creationOptions: { cwd: resource }, - processId: Promise.resolve(1), - dispose: noop, - show: noop, - sendText: noop, - } as unknown) as Terminal; - when(terminalManager.createTerminal(anything())).thenReturn(terminal); - when(documentManager.openTextDocument(initScriptPath)).thenReturn(Promise.resolve(document)); - when(documentManager.onDidChangeActiveTextEditor).thenReturn(activeEditorEvent.event); - shell = mock(); - interpreterService = mock(); - experimentService = mock(); - persistentStateFactory = mock(); - appEnvironment = mock(); - when(appEnvironment.shell).thenReturn('bash'); - notificationEnabled = mock>(); - terminalWriteEvent = new EventEmitter(); - when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( - instance(notificationEnabled), - ); - when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); - deactivatePrompt = new TerminalDeactivateLimitationPrompt( - instance(shell), - instance(persistentStateFactory), - [], - instance(interpreterService), - instance(appEnvironment), - instance(documentManager), - instance(terminalManager), - instance(experimentService), - ); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - }); - - test('When using cmd, do not show notification for the same', async () => { - reset(appEnvironment); - when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('When not in experiment, do not show notification for the same', async () => { - reset(experimentService); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); - - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification if notification is disabled', async () => { - when(notificationEnabled.value).thenReturn(false); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification when virtual env is not activated for terminal', async () => { - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Conda, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test("Disable notification if `Don't show again` is clicked", async () => { - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(notificationEnabled.updateValue(false)).once(); - }); - - test('Edit script correctly if `Edit