From ab4fca9344fb9c24d197e6f8eac200fd00c8fe4c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 8 Aug 2022 15:42:37 -0700 Subject: [PATCH 01/59] Ensure any labels or assignees do not carry over when transferring issues to `vscode-python` (#19656) * Ensure any labels or assignees do not carry over when transferring issues to repo * Fix indent * Make list of envs global * Minor edit * Run prettier * Add Eleanor to triage flow Co-authored-by: Karthik Nadig Co-authored-by: Karthik Nadig --- .github/workflows/getLabels.js | 17 ++++++++++++++++ .github/workflows/issue-labels.yml | 32 +++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/getLabels.js diff --git a/.github/workflows/getLabels.js b/.github/workflows/getLabels.js new file mode 100644 index 000000000000..2f8a21bae6fa --- /dev/null +++ b/.github/workflows/getLabels.js @@ -0,0 +1,17 @@ +/** + * To run this file: + * * npm install @octokit/rest + * * node .github/workflows/getLabels.js + */ + +const { Octokit } = require('@octokit/rest'); +const github = new Octokit(); +github.rest.issues + .listLabelsForRepo({ + owner: 'microsoft', + repo: 'vscode-python', + }) + .then((result) => { + const labels = result.data.map((label) => label.name); + console.log(JSON.stringify(labels)); + }); diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index d743d437428a..20f061828cbe 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -4,13 +4,18 @@ on: issues: types: [opened, reopened] +env: + # To update the list of labels, see `getLabels.js`. + REPO_LABELS: '["area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux"]' + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' + permissions: issues: write jobs: # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. add-classify-label: - name: "Add 'triage-needed'" + name: "Add 'triage-needed' and remove unrecognizable labels & assignees" runs-on: ubuntu-latest steps: - uses: actions/github-script@v6 @@ -37,3 +42,28 @@ jobs: } else { console.log('This issue already has a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label, do not add the "triage-needed" label.') } + const knownLabels = ${{ env.REPO_LABELS }} + const knownTriagers = ${{ env.TRIAGERS }} + for( const label of labels) { + if (!knownLabels.includes(label)) { + await github.rest.issues.deleteLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + }) + } + } + const currentAssignees = await github.rest.issues + .get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + .then((result) => result.data.assignees.map((a) => a.login)); + const assigneesToRemove = currentAssignees.filter(a => !knownTriagers.includes(a)); + github.rest.issues.removeAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: assigneesToRemove, + }); From 0ddc9812b14304362e96d37bd2891a5fec0fc8c5 Mon Sep 17 00:00:00 2001 From: Bill Schnurr Date: Tue, 9 Aug 2022 10:14:58 -0700 Subject: [PATCH 02/59] add more pylance settings to telemetry (#19658) --- src/client/telemetry/pylance.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index ce4723992c86..5b487da7563e 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -309,7 +309,12 @@ "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, } */ /* __GDPR__ From 822889bd5dd6068d6163c2d39f4c51bf9f58b2d8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 Aug 2022 10:20:24 -0700 Subject: [PATCH 03/59] Only remove assignees if issue doesn't have a needs or TPI label (#19659) --- .github/workflows/issue-labels.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 20f061828cbe..232cf3df83d2 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -39,6 +39,20 @@ jobs: issue_number: context.issue.number, labels: ['triage-needed'] }) + const currentAssignees = await github.rest.issues + .get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + .then((result) => result.data.assignees.map((a) => a.login)); + const assigneesToRemove = currentAssignees.filter(a => !knownTriagers.includes(a)); + github.rest.issues.removeAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: assigneesToRemove, + }); } else { console.log('This issue already has a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label, do not add the "triage-needed" label.') } @@ -53,17 +67,3 @@ jobs: }) } } - const currentAssignees = await github.rest.issues - .get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }) - .then((result) => result.data.assignees.map((a) => a.login)); - const assigneesToRemove = currentAssignees.filter(a => !knownTriagers.includes(a)); - github.rest.issues.removeAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - assignees: assigneesToRemove, - }); From bb54c9f2a0635da29f5a32f88171ce2643274d94 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 Aug 2022 13:12:14 -0700 Subject: [PATCH 04/59] Fix workflow for removing assignees (#19661) --- .github/workflows/issue-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 232cf3df83d2..505dd0b798f1 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -39,6 +39,7 @@ jobs: issue_number: context.issue.number, labels: ['triage-needed'] }) + const knownTriagers = ${{ env.TRIAGERS }} const currentAssignees = await github.rest.issues .get({ owner: context.repo.owner, @@ -57,7 +58,6 @@ jobs: console.log('This issue already has a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label, do not add the "triage-needed" label.') } const knownLabels = ${{ env.REPO_LABELS }} - const knownTriagers = ${{ env.TRIAGERS }} for( const label of labels) { if (!knownLabels.includes(label)) { await github.rest.issues.deleteLabel({ From 81578aa1dbbfb5ae68dd891069b1deef43e66737 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 Aug 2022 13:40:01 -0700 Subject: [PATCH 05/59] Disable removing labels until label list is upto-date (#19662) --- .github/workflows/issue-labels.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 505dd0b798f1..2e45c213d960 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -57,13 +57,3 @@ jobs: } else { console.log('This issue already has a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label, do not add the "triage-needed" label.') } - const knownLabels = ${{ env.REPO_LABELS }} - for( const label of labels) { - if (!knownLabels.includes(label)) { - await github.rest.issues.deleteLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label, - }) - } - } From d2ac072663e78a1bd749e757821cf830163fb00f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 Aug 2022 15:22:51 -0700 Subject: [PATCH 06/59] Fix the hardcoded labels on the repository (#19664) --- .github/workflows/getLabels.js | 10 +++++++++- .github/workflows/issue-labels.yml | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/getLabels.js b/.github/workflows/getLabels.js index 2f8a21bae6fa..99060e7205eb 100644 --- a/.github/workflows/getLabels.js +++ b/.github/workflows/getLabels.js @@ -2,6 +2,8 @@ * To run this file: * * npm install @octokit/rest * * node .github/workflows/getLabels.js + * + * This script assumes the maximum number of labels to be 100. */ const { Octokit } = require('@octokit/rest'); @@ -10,8 +12,14 @@ github.rest.issues .listLabelsForRepo({ owner: 'microsoft', repo: 'vscode-python', + per_page: 100, }) .then((result) => { const labels = result.data.map((label) => label.name); - console.log(JSON.stringify(labels)); + console.log( + '\nNumber of labels found:', + labels.length, + ", verify that it's the same as number of labels listed in https://github.com/microsoft/vscode-python/labels\n", + ); + console.log(JSON.stringify(labels), '\n'); }); diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 2e45c213d960..b10983dd0706 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -6,7 +6,7 @@ on: env: # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux"]' + REPO_LABELS: '["area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' permissions: @@ -57,3 +57,13 @@ jobs: } else { console.log('This issue already has a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label, do not add the "triage-needed" label.') } + const knownLabels = ${{ env.REPO_LABELS }} + for( const label of labels) { + if (!knownLabels.includes(label)) { + await github.rest.issues.deleteLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + }) + } + } From 344c912a1c15d07eb9b14bf749c7529a7fa0877b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 11 Aug 2022 14:48:23 -0700 Subject: [PATCH 07/59] Fix for telemetry JSON extraction. (#19675) Fix for JSON telemetry extraction. --- src/client/telemetry/pylance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 5b487da7563e..119c19b1e526 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -314,7 +314,7 @@ "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ /* __GDPR__ From 830cbe717c534a8182bf7533ad80407a94180452 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Mon, 15 Aug 2022 09:19:51 -0700 Subject: [PATCH 08/59] Update variables in Flask Debug Configuration (#19625) Update variables in Flask Debuug Configuration --- .../dynamicdebugConfigurationService.ts | 4 ++-- .../extension/configuration/providers/flaskLaunch.ts | 4 ++-- .../configuration/providers/flaskLaunch.unit.test.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index 450b7e5ee032..46937bdc65ba 100644 --- a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts @@ -54,9 +54,9 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf module: 'flask', env: { FLASK_APP: path.relative(folder.uri.fsPath, flaskPath), - FLASK_ENV: 'development', + FLASK_DEBUG: '1', }, - args: ['run', '--no-debugger'], + args: ['run', '--no-debugger', '--no-reload'], jinja: true, justMyCode: true, }); diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts index 07d5522f27ca..034308c73cda 100644 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -31,9 +31,9 @@ export class FlaskLaunchDebugConfigurationProvider implements IDebugConfiguratio module: 'flask', env: { FLASK_APP: application || 'app.py', - FLASK_ENV: 'development', + FLASK_DEBUG: '1', }, - args: ['run', '--no-debugger'], + args: ['run', '--no-debugger', '--no-reload'], jinja: true, justMyCode: true, }; diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts index da8213bd1bd4..c8beab640ab1 100644 --- a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts @@ -62,9 +62,9 @@ suite('Debugging - Configuration Provider Flask', () => { module: 'flask', env: { FLASK_APP: 'xyz.py', - FLASK_ENV: 'development', + FLASK_DEBUG: '1', }, - args: ['run', '--no-debugger'], + args: ['run', '--no-debugger', '--no-reload'], jinja: true, justMyCode: true, }; @@ -87,9 +87,9 @@ suite('Debugging - Configuration Provider Flask', () => { module: 'flask', env: { FLASK_APP: 'hello', - FLASK_ENV: 'development', + FLASK_DEBUG: '1', }, - args: ['run', '--no-debugger'], + args: ['run', '--no-debugger', '--no-reload'], jinja: true, justMyCode: true, }; @@ -112,9 +112,9 @@ suite('Debugging - Configuration Provider Flask', () => { module: 'flask', env: { FLASK_APP: 'app.py', - FLASK_ENV: 'development', + FLASK_DEBUG: '1', }, - args: ['run', '--no-debugger'], + args: ['run', '--no-debugger', '--no-reload'], jinja: true, justMyCode: true, }; From f0c55b774f70fba3f19debeeeda1f37febf481d1 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 17 Aug 2022 14:32:03 -0400 Subject: [PATCH 09/59] Adds the yml file necessary for the codereview bot (#19689) * Add code review chat * Prettier --- .github/workflows/pr-chat.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/pr-chat.yml diff --git a/.github/workflows/pr-chat.yml b/.github/workflows/pr-chat.yml new file mode 100644 index 000000000000..bff67b133a00 --- /dev/null +++ b/.github/workflows/pr-chat.yml @@ -0,0 +1,25 @@ +name: PR Chat +on: + pull_request_target: + types: [opened, ready_for_review, closed] + +jobs: + main: + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run Code Review Chat + uses: ./actions/code-review-chat + with: + token: ${{secrets.GITHUB_TOKEN}} + slack_token: ${{ secrets.SLACK_TOKEN }} + slack_bot_name: 'VSCodeBot' + notification_channel: codereview From c434140e5a0481d37ab2da7e82296b7d0fea8338 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 17 Aug 2022 11:59:18 -0700 Subject: [PATCH 10/59] Update debugger to latest v1.6.3. (#19698) --- pythonFiles/install_debugpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py index 593580d6a211..2a8594b4b52e 100644 --- a/pythonFiles/install_debugpy.py +++ b/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp39",) -DEBUGGER_VERSION = "1.6.2" # can also be "latest" +DEBUGGER_VERSION = "1.6.3" # can also be "latest" def _contains(s, parts=()): From bf9df043efa32c43b89e289854e4975a68fba5b9 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 17 Aug 2022 11:59:45 -0700 Subject: [PATCH 11/59] Delete test_plan.md (#19690) --- .github/test_plan.md | 335 ------------------------------------------- 1 file changed, 335 deletions(-) delete mode 100644 .github/test_plan.md diff --git a/.github/test_plan.md b/.github/test_plan.md deleted file mode 100644 index 498f3c071150..000000000000 --- a/.github/test_plan.md +++ /dev/null @@ -1,335 +0,0 @@ -# Test plan - -## Environment - -- OS: XXX (Windows, macOS, latest Ubuntu LTS) - - Shell: XXX (Command Prompt, PowerShell, bash, fish) -- Python - - Distribution: XXX (CPython, miniconda) - - Version: XXX (2.7, latest 3.x) -- VS Code: XXX (Insiders) - -## Tests - -**ALWAYS**: - -- Check the `Output` window under `Python` for logged errors -- Have `Developer Tools` open to detect any errors -- Consider running the tests in a multi-folder workspace -- Focus on in-development features (i.e. experimental debugger and language server) - -
- Scenarios - -### [Environment](https://code.visualstudio.com/docs/python/environments) - -#### Interpreters - -- [ ] Interpreter is [shown in the status bar](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) -- [ ] An interpreter can be manually specified using the [`Select Interpreter` command](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) -- [ ] Detected system-installed interpreters -- [ ] Detected an Anaconda installation -- [ ] (Linux/macOS) Detected all interpreters installed w/ [pyenv](https://github.com/pyenv/pyenv) detected -- [ ] [`"python.pythonPath"`](https://code.visualstudio.com/docs/python/environments#_manually-specifying-an-interpreter) triggers an update in the status bar -- [ ] `Run Python File in Terminal` -- [ ] `Run Selection/Line in Python Terminal` - - [ ] Right-click - - [ ] Command - - [ ] `Shift+Enter` - -#### Terminal - -Sample file: - -```python -import requests -request = requests.get("https://drive.google.com/uc?export=download&id=1_9On2-nsBQIw3JiY43sWbrF8EjrqrR4U") -with open("survey2017.zip", "wb") as file: - file.write(request.content) -import zipfile -with zipfile.ZipFile('survey2017.zip') as zip: - zip.extractall('survey2017') -import shutil, os -shutil.move('survey2017/survey_results_public.csv','survey2017.csv') -shutil.rmtree('survey2017') -os.remove('survey2017.zip') -``` - -- [ ] _Shift+Enter_ to send selected code in sample file to terminal works - -#### Virtual environments - -**ALWAYS**: - -- Use the latest version of Anaconda -- Realize that `conda` is slow -- Create an environment with a space in their path somewhere as well as upper and lowercase characters -- Make sure that you do not have `python.pythonPath` specified in your `settings.json` when testing automatic detection -- Do note that the `Select Interpreter` drop-down window scrolls - -- [ ] Detected a single virtual environment at the top-level of the workspace folder on Mac when when `python` command points to default Mac Python installation or `python` command fails in the terminal. - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) -- [ ] Detected a single virtual environment at the top-level of the workspace folder on Windows when `python` fails in the terminal. - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) -- [ ] Detected a single virtual environment at the top-level of the workspace folder - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) - - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works - - [ ] Steals focus - - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal - - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart -- [ ] Detect multiple virtual environments contained in the directory specified in `"python.venvPath"` -- [ ] Detected all [conda environments created with an interpreter](https://code.visualstudio.com/docs/python/environments#_conda-environments) - - [ ] Appropriate suffix label specified in status bar (e.g. `(condaenv)`) - - [ ] Prompted to install Pylint - - [ ] Asked whether to install using conda or pip - - [ ] Installs into environment - - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works - - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal - - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart -- [ ] (Linux/macOS until [`-m` is supported](https://github.com/Microsoft/vscode-python/issues/978)) Detected the virtual environment created by [pipenv](https://docs.pipenv.org/) - - [ ] Appropriate suffix label specified in status bar (e.g. `(pipenv)`) - - [ ] Prompt to install Pylint uses `pipenv install --dev` - - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works - - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal - - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart -- [ ] (Linux/macOS) Virtual environments created under `{workspaceFolder}/.direnv/python-{python_version}` are detected (for [direnv](https://direnv.net/) and its [`layout python3`](https://github.com/direnv/direnv/blob/master/stdlib.sh) support) - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) - -#### [Environment files](https://code.visualstudio.com/docs/python/environments#_environment-variable-definitions-file) - -Sample files: - -```python -# example.py -import os -print('Hello,', os.environ.get('WHO'), '!') -``` - -``` -# .env -WHO=world -PYTHONPATH=some/path/somewhere -SPAM='hello ${WHO}' -``` - -**ALWAYS**: - -- Make sure to use `Reload Window` between tests to reset your environment -- Note that environment files only apply under the debugger - -- [ ] Environment variables in a `.env` file are exposed when running under the debugger -- [ ] `"python.envFile"` allows for specifying an environment file manually -- [ ] `envFile` in a `launch.json` configuration works -- [ ] simple variable substitution works - -#### [Debugging](https://code.visualstudio.com/docs/python/environments#_python-interpreter-for-debugging) - -- [ ] `pythonPath` setting in your `launch.json` overrides your `python.pythonPath` default setting - -### [Linting](https://code.visualstudio.com/docs/python/linting) - -**ALWAYS**: - -- Check under the `Problems` tab to see e.g. if a linter is raising errors - -#### Language server - -- [ ] LS is downloaded using HTTP (no SSL) when the "http.proxyStrictSSL" setting is false -- [ ] An item with a cloud icon appears in the status bar indicating progress while downloading the language server -- [ ] Installing [`requests`](https://pypi.org/project/requests/) in virtual environment is detected - - [ ] Import of `requests` without package installed is flagged as unresolved - - [ ] Create a virtual environment - - [ ] Install `requests` into the virtual environment - -#### Linting - -**Note**: - -- You can use the `Run Linting` command to run a newly installed linter -- When the extension installs a new linter, it turns off all other linters - -- [ ] pylint works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] flake8 works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] mypy works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] pycodestyle works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] prospector works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] pydocstyle works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] pylama works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] 3 or more linters work simultaneously (make sure you have turned on the linters in your `settings.json`) - - [ ] `Run Linting` runs all activated linters - - [ ] `"python.linting.enabled": false` disables all linters - - [ ] The `Enable Linting` command changes `"python.linting.enabled"` -- [ ] `"python.linting.lintOnSave` works - -### [Editing](https://code.visualstudio.com/docs/python/editing) - -#### [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense) - -Please also test for general accuracy on the most "interesting" code you can find. - -- [ ] `"python.autoComplete.extraPaths"` works -- [ ] `"python.autocomplete.addBrackets": true` causes auto-completion of functions to append `()` -- [ ] Auto-completions works - -#### [Formatting](https://code.visualstudio.com/docs/python/editing#_formatting) - -Sample file: - -```python -# There should be _some_ change after running `Format Document`. -import os,sys; -def foo():pass -``` - -- [ ] Prompted to install a formatter if none installed and `Format Document` is run - - [ ] Installing `autopep8` works - - [ ] Installing `black` works - - [ ] Installing `yapf` works -- [ ] Formatters work with default settings (i.e. `"python.formatting.provider"` is specified but not matching `*Path`or `*Args` settings) - - [ ] autopep8 - - [ ] black - - [ ] yapf -- [ ] Formatters work when appropriate `*Path` and `*Args` settings are specified (use absolute paths; use `~` if possible) - - [ ] autopep8 - - [ ] black - - [ ] yapf -- [ ] `"editor.formatOnType": true` works and has expected results - -### [Debugging](https://code.visualstudio.com/docs/python/debugging) - -- [ ] [Configurations](https://code.visualstudio.com/docs/python/debugging#_debugging-specific-app-types) work (see [`package.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) and the `"configurationSnippets"` section for all of the possible configurations) -- [ ] Running code from start to finish w/ no special debugging options (e.g. no breakpoints) -- [ ] Breakpoint-like things - - [ ] Breakpoint - - [ ] Set - - [ ] Hit - - [ ] Conditional breakpoint - - [ ] Expression - - [ ] Set - - [ ] Hit - - [ ] Hit count - - [ ] Set - - [ ] Hit - - [ ] Logpoint - - [ ] Set - - [ ] Hit -- [ ] Stepping - - [ ] Over - - [ ] Into - - [ ] Out -- [ ] Can inspect variables - - [ ] Through hovering over variable in code - - [ ] `Variables` section of debugger sidebar -- [ ] [Remote debugging](https://code.visualstudio.com/docs/python/debugging#_remote-debugging) works - - [ ] ... over SSH - - [ ] ... on other branches -- [ ] [App Engine](https://code.visualstudio.com/docs/python/debugging#_google-app-engine-debugging) - -### [Unit testing](https://code.visualstudio.com/docs/python/unit-testing) - -#### [`unittest`](https://code.visualstudio.com/docs/python/unit-testing#_unittest-configuration-settings) - -```python -import unittest - -MODULE_SETUP = False - - -def setUpModule(): - global MODULE_SETUP - MODULE_SETUP = True - - -class PassingSetupTests(unittest.TestCase): - CLASS_SETUP = False - METHOD_SETUP = False - - @classmethod - def setUpClass(cls): - cls.CLASS_SETUP = True - - def setUp(self): - self.METHOD_SETUP = True - - def test_setup(self): - self.assertTrue(MODULE_SETUP) - self.assertTrue(self.CLASS_SETUP) - self.assertTrue(self.METHOD_SETUP) - - -class PassingTests(unittest.TestCase): - - def test_passing(self): - self.assertEqual(42, 42) - - def test_passing_still(self): - self.assertEqual("silly walk", "silly walk") - - -class FailingTests(unittest.TestCase): - - def test_failure(self): - self.assertEqual(42, -13) - - def test_failure_still(self): - self.assertEqual("I'm right!", "no, I am!") -``` - -- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner -- [ ] Tests are discovered (as shown by code lenses on each test) - - [ ] Code lens for a class runs all tests for that class - - [ ] Code lens for a method runs just that test - - [ ] `Run Test` works - - [ ] `Debug Test` works - - [ ] Module/suite setup methods are also run (run the `test_setup` method to verify) -- [ ] while debugging tests, an uncaught exception in a test does not - cause `debugpy` to raise `SystemExit` exception. - -#### [`pytest`](https://code.visualstudio.com/docs/python/unit-testing#_pytest-configuration-settings) - -```python -def test_passing(): - assert 42 == 42 - -def test_failure(): - assert 42 == -13 -``` - -- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner - - [ ] `pytest` gets installed -- [ ] Tests are discovered (as shown by code lenses on each test) - - [ ] `Run Test` works - - [ ] `Debug Test` works -- [ ] A `Diagnostic` is shown in the problems pane for each failed/skipped test - - [ ] The `Diagnostic`s are organized according to the file the test was executed from (not necessarily the file it was defined in) - - [ ] The appropriate `DiagnosticRelatedInformation` is shown for each `Diagnostic` - - [ ] The `DiagnosticRelatedInformation` reflects the traceback for the test - -#### General - -- [ ] Code lenses appears - - [ ] `Run Test` lens works (and status bar updates as appropriate) - - [ ] `Debug Test` lens works - - [ ] Appropriate ✔/❌ shown for each test -- [ ] Status bar is functioning - - [ ] Appropriate test results displayed - - [ ] `Run All Unit Tests` works - - [ ] `Discover Unit Tests` works (resets tests result display in status bar) - - [ ] `Run Unit Test Method ...` works - - [ ] `View Unit Test Output` works - - [ ] After having at least one failure, `Run Failed Tests` works -- [ ] `Configure Unit Tests` works - - [ ] quick pick for framework (and its settings) - - [ ] selected framework enabled in workspace settings - - [ ] framework's config added (and old config removed) - - [ ] other frameworks disabled in workspace settings -- [ ] `Configure Unit Tests` does not close if it loses focus -- [ ] Cancelling configuration does not leave incomplete settings -- [ ] The first `"request": "test"` entry in launch.json is used for running unit tests From 1bbe377d730442f9594020e25d57a257a75b078e Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Thu, 18 Aug 2022 22:16:30 -0700 Subject: [PATCH 12/59] Update Pylance settings telemetry properties (#19708) --- src/client/telemetry/pylance.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 119c19b1e526..2c6b2744a0de 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -301,20 +301,23 @@ "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, } */ /* __GDPR__ From 5cfa71d5e9aa9d3a26427106f770535492434aed Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Fri, 19 Aug 2022 17:01:17 -0700 Subject: [PATCH 13/59] Fix commas in pylance.ts json (#19718) --- src/client/telemetry/pylance.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 2c6b2744a0de..c7be1af23fa6 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -303,21 +303,21 @@ "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ /* __GDPR__ From b68bcd7dc5191f1a2732737474c6e95c15cf541a Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Wed, 24 Aug 2022 17:55:26 -0700 Subject: [PATCH 14/59] Logic to control Pylance auto-indent experiment (#19722) --- .../languageClientMiddlewareBase.ts | 6 ++ src/client/activation/node/analysisOptions.ts | 55 +++++++++++++++++++ .../node/languageClientMiddleware.ts | 29 +++++++++- src/client/common/application/types.ts | 3 +- src/client/common/application/workspace.ts | 12 +++- .../pylanceLSExtensionManager.ts | 1 + .../node/analysisOptions.unit.test.ts | 16 +++++- 7 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/client/activation/languageClientMiddlewareBase.ts b/src/client/activation/languageClientMiddlewareBase.ts index 29a49b8bd35d..67ec24701189 100644 --- a/src/client/activation/languageClientMiddlewareBase.ts +++ b/src/client/activation/languageClientMiddlewareBase.ts @@ -10,6 +10,7 @@ import { Middleware, ResponseError, } from 'vscode-languageclient'; +import { ConfigurationItem } from 'vscode-languageserver-protocol'; import { HiddenFilePrefix } from '../common/constants'; import { IConfigurationService } from '../common/types'; @@ -96,6 +97,8 @@ export class LanguageClientMiddlewareBase implements Middleware { settingDict._envPYTHONPATH = envPYTHONPATH; } } + + this.configurationHook(item, settings[i] as LSPObject); } return settings; @@ -107,6 +110,9 @@ export class LanguageClientMiddlewareBase implements Middleware { return undefined; } + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + protected configurationHook(_item: ConfigurationItem, _settings: LSPObject): void {} + private get connected(): Promise { return this.connectedPromise.promise; } diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index f06ed52b7b54..a410405d3131 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,18 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { ConfigurationTarget, extensions, WorkspaceConfiguration } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; +import * as semver from 'semver'; import { IWorkspaceService } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { IExperimentService } from '../../common/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; import { LspNotebooksExperiment } from './lspNotebooksExperiment'; +const EDITOR_CONFIG_SECTION = 'editor'; +const FORMAT_ON_TYPE_CONFIG_SETTING = 'formatOnType'; + export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor( lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService, + private readonly experimentService: IExperimentService, private readonly lspNotebooksExperiment: LspNotebooksExperiment, ) { super(lsOutputChannel, workspace); @@ -25,6 +33,53 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt trustedWorkspaceSupport: true, lspNotebooksSupport: this.lspNotebooksExperiment.isInNotebooksExperiment(), lspInteractiveWindowSupport: this.lspNotebooksExperiment.isInNotebooksExperimentWithInteractiveWindowSupport(), + autoIndentSupport: await this.isAutoIndentEnabled(), } as unknown) as LanguageClientOptions; } + + private async isAutoIndentEnabled() { + const editorConfig = this.getPythonSpecificEditorSection(); + let formatOnTypeEffectiveValue = editorConfig.get(FORMAT_ON_TYPE_CONFIG_SETTING); + const formatOnTypeInspect = editorConfig.inspect(FORMAT_ON_TYPE_CONFIG_SETTING); + const formatOnTypeSetForPython = formatOnTypeInspect?.globalLanguageValue !== undefined; + + const inExperiment = await this.isInAutoIndentExperiment(); + + if (inExperiment !== formatOnTypeSetForPython) { + if (inExperiment) { + await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, true); + } else if (formatOnTypeInspect?.globalLanguageValue !== false) { + await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, undefined); + } + + formatOnTypeEffectiveValue = this.getPythonSpecificEditorSection().get(FORMAT_ON_TYPE_CONFIG_SETTING); + } + + return inExperiment && formatOnTypeEffectiveValue; + } + + private async isInAutoIndentExperiment(): Promise { + if (await this.experimentService.inExperiment('pylanceAutoIndent')) { + return true; + } + + const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version; + return pylanceVersion && semver.prerelease(pylanceVersion)?.includes('dev'); + } + + private getPythonSpecificEditorSection() { + return this.workspace.getConfiguration(EDITOR_CONFIG_SECTION, undefined, /* languageSpecific */ true); + } + + private static async setPythonSpecificFormatOnType( + editorConfig: WorkspaceConfiguration, + value: boolean | undefined, + ) { + await editorConfig.update( + FORMAT_ON_TYPE_CONFIG_SETTING, + value, + ConfigurationTarget.Global, + /* overrideInLanguage */ true, + ); + } } diff --git a/src/client/activation/node/languageClientMiddleware.ts b/src/client/activation/node/languageClientMiddleware.ts index e1e9cb447bc1..9c1d4c468191 100644 --- a/src/client/activation/node/languageClientMiddleware.ts +++ b/src/client/activation/node/languageClientMiddleware.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { IJupyterExtensionDependencyManager } from '../../common/application/types'; +import { ConfigurationItem, LanguageClient, LSPObject } from 'vscode-languageclient/node'; +import { IJupyterExtensionDependencyManager, IWorkspaceService } from '../../common/application/types'; import { IServiceContainer } from '../../ioc/types'; import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; import { traceLog } from '../../logging'; @@ -19,6 +19,8 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { private readonly jupyterExtensionIntegration: JupyterExtensionIntegration; + private readonly workspaceService: IWorkspaceService; + public constructor( serviceContainer: IServiceContainer, private getClient: () => LanguageClient | undefined, @@ -26,6 +28,8 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { ) { super(serviceContainer, LanguageServerType.Node, serverVersion); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.lspNotebooksExperiment = serviceContainer.get(LspNotebooksExperiment); this.setupHidingMiddleware(serviceContainer); @@ -82,4 +86,25 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { return result; } + + // eslint-disable-next-line class-methods-use-this + protected configurationHook(item: ConfigurationItem, settings: LSPObject): void { + if (item.section === 'editor') { + if (this.workspaceService) { + // Get editor.formatOnType using Python language id so [python] setting + // will be honored if present. + const editorConfig = this.workspaceService.getConfiguration( + item.section, + undefined, + /* languageSpecific */ true, + ); + + const settingDict: LSPObject & { formatOnType?: boolean } = settings as LSPObject & { + formatOnType: boolean; + }; + + settingDict.formatOnType = editorConfig.get('formatOnType'); + } + } + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index a91aeed75b04..256404810237 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -837,9 +837,10 @@ export interface IWorkspaceService { * * @param section A dot-separated identifier. * @param resource A resource for which the configuration is asked for + * @param languageSpecific Should the [python] language-specific settings be obtained? * @return The full configuration or a subset. */ - getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration; + getConfiguration(section?: string, resource?: Uri, languageSpecific?: boolean): WorkspaceConfiguration; /** * Opens an untitled text document. The editor will prompt the user for a file diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 69d5dff965a3..0a5fd8d81816 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -39,8 +39,16 @@ export class WorkspaceService implements IWorkspaceService { public get workspaceFile() { return workspace.workspaceFile; } - public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { - return workspace.getConfiguration(section, resource || null); + public getConfiguration( + section?: string, + resource?: Uri, + languageSpecific: boolean = false, + ): WorkspaceConfiguration { + if (languageSpecific) { + return workspace.getConfiguration(section, { uri: resource, languageId: 'python' }); + } else { + return workspace.getConfiguration(section, resource); + } } public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { return uri ? workspace.getWorkspaceFolder(uri) : undefined; diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index faa1bb75c4bc..2cc74308feea 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -58,6 +58,7 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities this.analysisOptions = new NodeLanguageServerAnalysisOptions( outputChannel, workspaceService, + experimentService, lspNotebooksExperiment, ); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); diff --git a/src/test/activation/node/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts index 0518fac170e9..8d16f0c0d9c9 100644 --- a/src/test/activation/node/analysisOptions.unit.test.ts +++ b/src/test/activation/node/analysisOptions.unit.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { assert, expect } from 'chai'; import * as typemoq from 'typemoq'; -import { WorkspaceFolder } from 'vscode'; +import { WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; import { DocumentFilter } from 'vscode-languageclient/node'; import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/node/analysisOptions'; @@ -10,7 +10,7 @@ import { LspNotebooksExperiment } from '../../../client/activation/node/lspNoteb import { ILanguageServerOutputChannel } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { IOutputChannel } from '../../../client/common/types'; +import { IExperimentService, IOutputChannel } from '../../../client/common/types'; suite('Pylance Language Server - Analysis Options', () => { class TestClass extends NodeLanguageServerAnalysisOptions { @@ -32,17 +32,27 @@ suite('Pylance Language Server - Analysis Options', () => { let outputChannel: IOutputChannel; let lsOutputChannel: typemoq.IMock; let workspace: typemoq.IMock; + let experimentService: IExperimentService; let lspNotebooksExperiment: typemoq.IMock; setup(() => { outputChannel = typemoq.Mock.ofType().object; workspace = typemoq.Mock.ofType(); workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); + const workspaceConfig = typemoq.Mock.ofType(); + workspace.setup((w) => w.getConfiguration('editor', undefined, true)).returns(() => workspaceConfig.object); + workspaceConfig.setup((w) => w.get('formatOnType')).returns(() => true); lsOutputChannel = typemoq.Mock.ofType(); lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel); + experimentService = typemoq.Mock.ofType().object; lspNotebooksExperiment = typemoq.Mock.ofType(); lspNotebooksExperiment.setup((l) => l.isInNotebooksExperiment()).returns(() => false); - analysisOptions = new TestClass(lsOutputChannel.object, workspace.object, lspNotebooksExperiment.object); + analysisOptions = new TestClass( + lsOutputChannel.object, + workspace.object, + experimentService, + lspNotebooksExperiment.object, + ); }); test('Workspace folder is undefined', () => { From 3bf4dae72819d97e28a3a1358042cf5c3efd5894 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 25 Aug 2022 15:31:47 -0700 Subject: [PATCH 15/59] Test server should bind to localhost. (#19735) * Test server should bind to localhost. * Fix tests. * Some logging for easier diagnosis. --- .../testing/testController/common/server.ts | 23 +++++++++++------- .../testing/testController/common/types.ts | 1 + .../testing/testController/common/utils.ts | 2 -- .../testing/testController/controller.ts | 5 ++-- .../testController/server.unit.test.ts | 24 ++++++++++++++----- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 8e6d2fac3829..adf5bba1a33c 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -12,7 +12,6 @@ import { } from '../../../common/process/types'; import { traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; -import { DEFAULT_TEST_PORT } from './utils'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -23,13 +22,11 @@ export class PythonTestServer implements ITestServer, Disposable { private server: http.Server; - public port: number; + private ready: Promise; constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { this.uuids = new Map(); - this.port = DEFAULT_TEST_PORT; - const requestListener: http.RequestListener = async (request, response) => { const buffers = []; @@ -59,11 +56,21 @@ export class PythonTestServer implements ITestServer, Disposable { }; this.server = http.createServer(requestListener); - this.server.listen(() => { - this.port = (this.server.address() as net.AddressInfo).port; + this.ready = new Promise((resolve, _reject) => { + this.server.listen(undefined, 'localhost', () => { + resolve(); + }); }); } + public serverReady(): Promise { + return this.ready; + } + + public getPort(): number { + return (this.server.address() as net.AddressInfo).port; + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -98,7 +105,7 @@ export class PythonTestServer implements ITestServer, Disposable { args = [ options.command.script, '--port', - this.port.toString(), + this.getPort().toString(), '--uuid', uuid, '--testids', @@ -106,7 +113,7 @@ export class PythonTestServer implements ITestServer, Disposable { ].concat(options.command.args); } else { // if not case of execution, go with the normal args - args = [options.command.script, '--port', this.port.toString(), '--uuid', uuid].concat( + args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( options.command.args, ); } diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 5a7a168b146e..064307ca8d9a 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -160,6 +160,7 @@ export type TestCommandOptions = { export interface ITestServer { readonly onDataReceived: Event; sendCommand(options: TestCommandOptions): Promise; + serverReady(): Promise; } export interface ITestDiscoveryAdapter { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f2c201dc9e0f..13fc76a37199 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -export const DEFAULT_TEST_PORT = 45454; - export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 1e4321e9ac7c..fafdd3fafe7e 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -142,14 +142,15 @@ export class PythonTestController implements ITestController, IExtensionSingleAc return this.refreshTestData(undefined, { forceRefresh: true }); }; - // this.pythonTestServer = new PythonTestServer(this.pythonExecFactory); // old way where debugLauncher did not have to be passed this.pythonTestServer = new PythonTestServer(this.pythonExecFactory, this.debugLauncher); } public async activate(): Promise { + traceVerbose('Waiting for test server to start...'); + await this.pythonTestServer.serverReady(); + traceVerbose('Test server started.'); const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; workspaces.forEach((workspace) => { - console.warn(`instantiating test adapters - workspace name: ${workspace.name}`); const settings = this.configSettings.getSettings(workspace.uri); let discoveryAdapter: ITestDiscoveryAdapter; diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 125500fd0ab7..59aeeda333b2 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -56,9 +56,10 @@ suite('Python Test Server', () => { }; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); await server.sendCommand(options); - const { port } = server; + const port = server.getPort(); assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); }); @@ -78,10 +79,11 @@ suite('Python Test Server', () => { }; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); await server.sendCommand(options); - const { port } = server; + const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); assert.deepStrictEqual(output, [expected]); @@ -102,6 +104,8 @@ suite('Python Test Server', () => { }; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { eventData = JSON.parse(data); }); @@ -123,6 +127,8 @@ suite('Python Test Server', () => { let response; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { response = data; deferred.resolve(); @@ -131,7 +137,7 @@ suite('Python Test Server', () => { await server.sendCommand(options); // Send data back. - const { port } = server; + const port = server.getPort(); const requestOptions = { hostname: 'localhost', method: 'POST', @@ -162,6 +168,8 @@ suite('Python Test Server', () => { let response; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { response = data; deferred.resolve(); @@ -170,7 +178,7 @@ suite('Python Test Server', () => { await server.sendCommand(options); // Send data back. - const { port } = server; + const port = server.getPort(); const requestOptions = { hostname: 'localhost', method: 'POST', @@ -202,6 +210,8 @@ suite('Python Test Server', () => { let response; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { response = data; deferred.resolve(); @@ -210,7 +220,7 @@ suite('Python Test Server', () => { await server.sendCommand(options); // Send data back. - const { port } = server; + const port = server.getPort(); const requestOptions = { hostname: 'localhost', method: 'POST', @@ -241,6 +251,8 @@ suite('Python Test Server', () => { let response; server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { response = data; deferred.resolve(); @@ -249,7 +261,7 @@ suite('Python Test Server', () => { await server.sendCommand(options); // Send data back. - const { port } = server; + const port = server.getPort(); const requestOptions = { hostname: 'localhost', method: 'POST', From cf70a1542dad2f38038c1abdab76d4442cbb49c4 Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Thu, 25 Aug 2022 17:50:46 -0700 Subject: [PATCH 16/59] Prevent auto-indent inExperiment method from returning undefined (#19739) --- src/client/activation/node/analysisOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index a410405d3131..3dcd294fd7ac 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -64,7 +64,7 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt } const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version; - return pylanceVersion && semver.prerelease(pylanceVersion)?.includes('dev'); + return pylanceVersion && semver.prerelease(pylanceVersion)?.includes('dev') === true; } private getPythonSpecificEditorSection() { From 5d8a3cb2002a13ac673e99455feec388811cfc14 Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Thu, 25 Aug 2022 23:18:13 -0700 Subject: [PATCH 17/59] Fix `any` type issue that caused experiment bug to be missed (#19740) --- src/client/activation/node/analysisOptions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 3dcd294fd7ac..815ca73ff7eb 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -63,8 +63,8 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt return true; } - const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version; - return pylanceVersion && semver.prerelease(pylanceVersion)?.includes('dev') === true; + const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version as string; + return pylanceVersion !== undefined && semver.prerelease(pylanceVersion)?.includes('dev') === true; } private getPythonSpecificEditorSection() { From 3ebf8107cb888768daf7cab0a524495cdd54e155 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 26 Aug 2022 12:08:26 -0700 Subject: [PATCH 18/59] Check telemetry metadata in CI (#19738) --- .github/workflows/telemetry.yml | 26 +++ package-lock.json | 320 ++++---------------------------- package.json | 3 +- 3 files changed, 66 insertions(+), 283 deletions(-) create mode 100644 .github/workflows/telemetry.yml diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml new file mode 100644 index 000000000000..fceb6227ecbb --- /dev/null +++ b/.github/workflows/telemetry.yml @@ -0,0 +1,26 @@ +name: 'Telemetry' +on: + pull_request: + paths: + - 'src/client/telemetry/*.ts' + push: + branches-ignore: + - 'main' + - 'release*' + paths: + - 'src/client/telemetry/*.ts' + +jobs: + check-metdata: + name: 'Check metadata' + runs-on: 'ubuntu-latest' + + steps: + - uses: 'actions/checkout@v3' + + - uses: 'actions/setup-node@v3' + with: + node-version: 'lts/*' + + - name: 'Run vscode-telemetry-extractor' + run: 'npx --package=@vscode/telemetry-extractor --yes vscode-telemetry-extractor -s src/client/telemetry/' diff --git a/package-lock.json b/package-lock.json index 1006c8056e19..2b0b2968119e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/telemetry-extractor": "^1.9.7", + "@vscode/telemetry-extractor": ">=1.9.8", "@vscode/test-electron": "^2.1.3", "chai": "^4.1.2", "chai-arrays": "^2.0.0", @@ -119,7 +119,6 @@ "vsce": "^2.6.6", "vscode-debugadapter-testsupport": "^1.27.0", "vscode-nls-dev": "^4.0.0", - "vscode-telemetry-extractor": "^1.9.5", "webpack": "^5.70.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -584,48 +583,6 @@ "node": ">= 6" } }, - "node_modules/@ts-morph/common": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", - "integrity": "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.7", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@ts-morph/common/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@ts-morph/common/node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, "node_modules/@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -1179,39 +1136,36 @@ } }, "node_modules/@vscode/telemetry-extractor": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vscode/telemetry-extractor/-/telemetry-extractor-1.9.7.tgz", - "integrity": "sha512-OsYcvwJYKtBN3vq9ZoS520VwvceVL6G2SHxKKWSyD8Py6ppgmWm0YXy4w2LLxKukM/iaQS1GZ5se1LVS5VxK8A==", + "version": "1.9.8", + "resolved": "https://registry.npmjs.org/@vscode/telemetry-extractor/-/telemetry-extractor-1.9.8.tgz", + "integrity": "sha512-L27/fgC/gM7AY6AXriFGrznnX1M4Nc7VmHabYinDPoJDQYLjbSEDDVjjlSS6BiVkzc3OrFQStqXpHBhImis2eQ==", "dev": true, "dependencies": { "@vscode/ripgrep": "^1.14.2", "command-line-args": "^5.2.1", - "ts-morph": "^14.0.0" + "ts-morph": "^15.1.0" }, "bin": { "vscode-telemetry-extractor": "out/extractor.js" } }, "node_modules/@vscode/telemetry-extractor/node_modules/@ts-morph/common": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.13.0.tgz", - "integrity": "sha512-fEJ6j7Cu8yiWjA4UmybOBH9Efgb/64ZTWuvCF4KysGu4xz8ettfyaqFt8WZ1btCxXsGZJjZ2/3svOF6rL+UFdQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.16.0.tgz", + "integrity": "sha512-SgJpzkTgZKLKqQniCjLaE3c2L2sdL7UShvmTmPBejAKd2OKV/yfMpQ2IWpAuA+VY5wy7PkSUaEObIqEK6afFuw==", "dev": true, "dependencies": { "fast-glob": "^3.2.11", - "minimatch": "^5.0.1", + "minimatch": "^5.1.0", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "node_modules/@vscode/telemetry-extractor/node_modules/code-block-writer": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.0.tgz", - "integrity": "sha512-GEqWvEWWsOvER+g9keO4ohFoD3ymwyCnqY3hoTr7GZipYFwEhMHJw+TtV0rfgRhNImM6QWZGO2XYjlJVyYT62w==", - "dev": true, - "dependencies": { - "tslib": "2.3.1" - } + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true }, "node_modules/@vscode/telemetry-extractor/node_modules/mkdirp": { "version": "1.0.4", @@ -1232,21 +1186,15 @@ "dev": true }, "node_modules/@vscode/telemetry-extractor/node_modules/ts-morph": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-14.0.0.tgz", - "integrity": "sha512-tO8YQ1dP41fw8GVmeQAdNsD8roZi1JMqB7YwZrqU856DvmG5/710e41q2XauzTYrygH9XmMryaFeLo+kdCziyA==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-15.1.0.tgz", + "integrity": "sha512-RBsGE2sDzUXFTnv8Ba22QfeuKbgvAGJFuTN7HfmIRUkgT/NaVLfDM/8OFm2NlFkGlWEXdpW5OaFIp1jvqdDuOg==", "dev": true, "dependencies": { - "@ts-morph/common": "~0.13.0", + "@ts-morph/common": "~0.16.0", "code-block-writer": "^11.0.0" } }, - "node_modules/@vscode/telemetry-extractor/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, "node_modules/@vscode/test-electron": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", @@ -1495,15 +1443,6 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -3275,12 +3214,6 @@ "readable-stream": "^2.3.5" } }, - "node_modules/code-block-writer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", - "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", - "dev": true - }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -9218,9 +9151,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -13558,16 +13491,6 @@ "lodash": "^4.17.5" } }, - "node_modules/ts-morph": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-12.2.0.tgz", - "integrity": "sha512-WHXLtFDcIRwoqaiu0elAoZ/AmI+SwwDafnPKjgJmdwJ2gRVO0jMKBt88rV2liT/c6MTsXyuWbGFiHe9MRddWJw==", - "dev": true, - "dependencies": { - "@ts-morph/common": "~0.11.1", - "code-block-writer": "^10.1.1" - } - }, "node_modules/ts-node": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", @@ -14659,48 +14582,6 @@ "node": ">=12" } }, - "node_modules/vscode-ripgrep": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/vscode-ripgrep/-/vscode-ripgrep-1.13.2.tgz", - "integrity": "sha512-RlK9U87EokgHfiOjDQ38ipQQX936gWOcWPQaJpYf+kAkz1PQ1pK2n7nhiscdOmLu6XGjTs7pWFJ/ckonpN7twQ==", - "deprecated": "This package has been renamed to @vscode/ripgrep, please update to the new name", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "https-proxy-agent": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/vscode-ripgrep/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vscode-ripgrep/node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/vscode-tas-client": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.22.tgz", @@ -14712,21 +14593,6 @@ "vscode": "^1.19.1" } }, - "node_modules/vscode-telemetry-extractor": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.9.5.tgz", - "integrity": "sha512-saUkRZrXVi9sKNqT6xqjky3oqybrj6ipdRCA257Ao1MgU260A2K4mD5kEZhupBdSD4+t+QwCv8WCFIcZDRD0Aw==", - "deprecated": "This package has been renamed to @vscode/telemetry-extractor, please update to the new name", - "dev": true, - "dependencies": { - "command-line-args": "^5.2.0", - "ts-morph": "^12.2.0", - "vscode-ripgrep": "^1.12.1" - }, - "bin": { - "vscode-telemetry-extractor": "out/extractor.js" - } - }, "node_modules/vscode-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz", @@ -15874,41 +15740,6 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, - "@ts-morph/common": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", - "integrity": "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==", - "dev": true, - "requires": { - "fast-glob": "^3.2.7", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - } - } - }, "@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -16363,36 +16194,33 @@ } }, "@vscode/telemetry-extractor": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vscode/telemetry-extractor/-/telemetry-extractor-1.9.7.tgz", - "integrity": "sha512-OsYcvwJYKtBN3vq9ZoS520VwvceVL6G2SHxKKWSyD8Py6ppgmWm0YXy4w2LLxKukM/iaQS1GZ5se1LVS5VxK8A==", + "version": "1.9.8", + "resolved": "https://registry.npmjs.org/@vscode/telemetry-extractor/-/telemetry-extractor-1.9.8.tgz", + "integrity": "sha512-L27/fgC/gM7AY6AXriFGrznnX1M4Nc7VmHabYinDPoJDQYLjbSEDDVjjlSS6BiVkzc3OrFQStqXpHBhImis2eQ==", "dev": true, "requires": { "@vscode/ripgrep": "^1.14.2", "command-line-args": "^5.2.1", - "ts-morph": "^14.0.0" + "ts-morph": "^15.1.0" }, "dependencies": { "@ts-morph/common": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.13.0.tgz", - "integrity": "sha512-fEJ6j7Cu8yiWjA4UmybOBH9Efgb/64ZTWuvCF4KysGu4xz8ettfyaqFt8WZ1btCxXsGZJjZ2/3svOF6rL+UFdQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.16.0.tgz", + "integrity": "sha512-SgJpzkTgZKLKqQniCjLaE3c2L2sdL7UShvmTmPBejAKd2OKV/yfMpQ2IWpAuA+VY5wy7PkSUaEObIqEK6afFuw==", "dev": true, "requires": { "fast-glob": "^3.2.11", - "minimatch": "^5.0.1", + "minimatch": "^5.1.0", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "code-block-writer": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.0.tgz", - "integrity": "sha512-GEqWvEWWsOvER+g9keO4ohFoD3ymwyCnqY3hoTr7GZipYFwEhMHJw+TtV0rfgRhNImM6QWZGO2XYjlJVyYT62w==", - "dev": true, - "requires": { - "tslib": "2.3.1" - } + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true }, "mkdirp": { "version": "1.0.4", @@ -16407,20 +16235,14 @@ "dev": true }, "ts-morph": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-14.0.0.tgz", - "integrity": "sha512-tO8YQ1dP41fw8GVmeQAdNsD8roZi1JMqB7YwZrqU856DvmG5/710e41q2XauzTYrygH9XmMryaFeLo+kdCziyA==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-15.1.0.tgz", + "integrity": "sha512-RBsGE2sDzUXFTnv8Ba22QfeuKbgvAGJFuTN7HfmIRUkgT/NaVLfDM/8OFm2NlFkGlWEXdpW5OaFIp1jvqdDuOg==", "dev": true, "requires": { - "@ts-morph/common": "~0.13.0", + "@ts-morph/common": "~0.16.0", "code-block-writer": "^11.0.0" } - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true } } }, @@ -16643,12 +16465,6 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, - "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true - }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -18035,12 +17851,6 @@ "readable-stream": "^2.3.5" } }, - "code-block-writer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", - "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", - "dev": true - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -22751,9 +22561,9 @@ "dev": true }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "requires": { "brace-expansion": "^2.0.1" }, @@ -26142,16 +25952,6 @@ "lodash": "^4.17.5" } }, - "ts-morph": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-12.2.0.tgz", - "integrity": "sha512-WHXLtFDcIRwoqaiu0elAoZ/AmI+SwwDafnPKjgJmdwJ2gRVO0jMKBt88rV2liT/c6MTsXyuWbGFiHe9MRddWJw==", - "dev": true, - "requires": { - "@ts-morph/common": "~0.11.1", - "code-block-writer": "^10.1.1" - } - }, "ts-node": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", @@ -27032,37 +26832,6 @@ } } }, - "vscode-ripgrep": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/vscode-ripgrep/-/vscode-ripgrep-1.13.2.tgz", - "integrity": "sha512-RlK9U87EokgHfiOjDQ38ipQQX936gWOcWPQaJpYf+kAkz1PQ1pK2n7nhiscdOmLu6XGjTs7pWFJ/ckonpN7twQ==", - "dev": true, - "requires": { - "https-proxy-agent": "^4.0.0", - "proxy-from-env": "^1.1.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "requires": { - "agent-base": "5", - "debug": "4" - } - } - } - }, "vscode-tas-client": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.22.tgz", @@ -27071,17 +26840,6 @@ "tas-client": "0.1.21" } }, - "vscode-telemetry-extractor": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.9.5.tgz", - "integrity": "sha512-saUkRZrXVi9sKNqT6xqjky3oqybrj6ipdRCA257Ao1MgU260A2K4mD5kEZhupBdSD4+t+QwCv8WCFIcZDRD0Aw==", - "dev": true, - "requires": { - "command-line-args": "^5.2.0", - "ts-morph": "^12.2.0", - "vscode-ripgrep": "^1.12.1" - } - }, "vscode-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz", diff --git a/package.json b/package.json index 48919bb4c1bb..ccadcbb3c0a3 100644 --- a/package.json +++ b/package.json @@ -1849,7 +1849,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/telemetry-extractor": "^1.9.7", + "@vscode/telemetry-extractor": ">=1.9.8", "@vscode/test-electron": "^2.1.3", "chai": "^4.1.2", "chai-arrays": "^2.0.0", @@ -1895,7 +1895,6 @@ "vsce": "^2.6.6", "vscode-debugadapter-testsupport": "^1.27.0", "vscode-nls-dev": "^4.0.0", - "vscode-telemetry-extractor": "^1.9.5", "webpack": "^5.70.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", From 8e19c478f9cfd90972e93b585833c9ccde5d2921 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 26 Aug 2022 13:50:13 -0700 Subject: [PATCH 19/59] Fix a YAML mistake in a GH workflow file (#19747) --- .github/workflows/telemetry.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index fceb6227ecbb..95a014790d75 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -1,8 +1,8 @@ name: 'Telemetry' on: pull_request: - paths: - - 'src/client/telemetry/*.ts' + paths: + - 'src/client/telemetry/*.ts' push: branches-ignore: - 'main' From fdf90cf7b5a8326e5133ad47a4ec214bebf64c60 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Mon, 29 Aug 2022 09:56:19 -0700 Subject: [PATCH 20/59] Enable notebooks behind LSP by default (#19750) --- package.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ccadcbb3c0a3..40d9830300af 100644 --- a/package.json +++ b/package.json @@ -916,12 +916,9 @@ }, "python.pylanceLspNotebooksEnabled": { "type": "boolean", - "default": false, + "default": true, "description": "%python.pylanceLspNotebooksEnabled.description%", - "scope": "machine", - "tags": [ - "experimental" - ] + "scope": "machine" }, "python.sortImports.args": { "default": [], From c82a3a9c04af43fb6aa1d6c615196614ccf7802f Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 29 Aug 2022 11:02:35 -0700 Subject: [PATCH 21/59] Update main version for release candidate. (#19752) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b0b2968119e..0c388e92e509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2022.13.0-dev", + "version": "2022.14.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2022.13.0-dev", + "version": "2022.14.0-rc", "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", diff --git a/package.json b/package.json index 40d9830300af..39192e3c8cd2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2022.13.0-dev", + "version": "2022.14.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 49925bc1cab41034e33b0e7be81b4f5194ba79b5 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 29 Aug 2022 11:37:06 -0700 Subject: [PATCH 22/59] Update main to next pre-release. (#19753) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c388e92e509..13ab8fdf9ef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2022.14.0-rc", + "version": "2022.15.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2022.14.0-rc", + "version": "2022.15.0-dev", "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", diff --git a/package.json b/package.json index 39192e3c8cd2..02d57305c323 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2022.14.0-rc", + "version": "2022.15.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From c35c423af9da6899bc9a601b0ef3d9ec0570088f Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 29 Aug 2022 13:39:47 -0700 Subject: [PATCH 23/59] Re-assign Kim-Adeline's telemetry (#19749) --- src/client/telemetry/index.ts | 48 +++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7f056894f79e..84d996f3c728 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1388,7 +1388,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "language_server_enabled" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.LANGUAGE_SERVER_ENABLED]: { @@ -1399,7 +1399,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "language_server_ready" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.LANGUAGE_SERVER_READY]: { @@ -1410,7 +1410,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "language_server_startup" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.LANGUAGE_SERVER_STARTUP]: { @@ -1421,7 +1421,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "language_server_telemetry" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.LANGUAGE_SERVER_TELEMETRY]: unknown; @@ -1432,7 +1432,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "language_server_request" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.LANGUAGE_SERVER_REQUEST]: unknown; @@ -1441,7 +1441,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "jedi_language_server.enabled" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.JEDI_LANGUAGE_SERVER_ENABLED]: { @@ -1452,7 +1452,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "jedi_language_server.ready" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.JEDI_LANGUAGE_SERVER_READY]: { @@ -1463,7 +1463,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "jedi_language_server.startup" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.JEDI_LANGUAGE_SERVER_STARTUP]: { @@ -1475,7 +1475,7 @@ export interface IEventNamePropertyMapping { * This event also has a measure, "resultLength", which records the number of completions provided. */ /* __GDPR__ - "jedi_language_server.request" : { "owner": "kimadeline" } + "jedi_language_server.request" : { "owner": "karthiknadig" } */ [EventName.JEDI_LANGUAGE_SERVER_REQUEST]: unknown; /** @@ -1483,7 +1483,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "extension_survey_prompt" : { - "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } } */ [EventName.EXTENSION_SURVEY_PROMPT]: { @@ -1524,7 +1524,7 @@ export interface IEventNamePropertyMapping { * Telemetry event sent if and when user configure tests command. This command can be trigerred from multiple places in the extension. (Command palette, prompt etc.) */ /* __GDPR__ - "unittest.configure" : { "owner": "kimadeline" } + "unittest.configure" : { "owner": "eleanorjboyd" } */ [EventName.UNITTEST_CONFIGURE]: never | undefined; /** @@ -1532,9 +1532,9 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "unittest.configuring" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "kimadeline" } + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "eleanorjboyd" } } */ [EventName.UNITTEST_CONFIGURING]: { @@ -1612,7 +1612,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "unittest.discovery.trigger" : { - "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } } */ [EventName.UNITTEST_DISCOVERY_TRIGGER]: { @@ -1633,7 +1633,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "unittest.discovering" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } } */ [EventName.UNITTEST_DISCOVERING]: { @@ -1649,8 +1649,8 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "unittest.discovery.done" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" }, - "failed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "failed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } } */ [EventName.UNITTEST_DISCOVERY_DONE]: { @@ -1671,7 +1671,7 @@ export interface IEventNamePropertyMapping { * Telemetry event sent when cancelling discovering tests */ /* __GDPR__ - "unittest.discovery.stop" : { "owner": "kimadeline" } + "unittest.discovery.stop" : { "owner": "eleanorjboyd" } */ [EventName.UNITTEST_DISCOVERING_STOP]: never | undefined; /** @@ -1679,8 +1679,8 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "unittest.run" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" }, - "debugging" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "debugging" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } } */ [EventName.UNITTEST_RUN]: { @@ -1697,21 +1697,21 @@ export interface IEventNamePropertyMapping { * Telemetry event sent when cancelling running tests */ /* __GDPR__ - "unittest.run.stop" : { "owner": "kimadeline" } + "unittest.run.stop" : { "owner": "eleanorjboyd" } */ [EventName.UNITTEST_RUN_STOP]: never | undefined; /** * Telemetry event sent when run all failed test command is triggered */ /* __GDPR__ - "unittest.run.all_failed" : { "owner": "kimadeline" } + "unittest.run.all_failed" : { "owner": "eleanorjboyd" } */ [EventName.UNITTEST_RUN_ALL_FAILED]: never | undefined; /** * Telemetry event sent when testing is disabled for a workspace. */ /* __GDPR__ - "unittest.disabled" : { "owner": "kimadeline" } + "unittest.disabled" : { "owner": "eleanorjboyd" } */ [EventName.UNITTEST_DISABLED]: never | undefined; /* From 9e6788a696fa8d05b280e4cf547bc22b01cc73e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Tue, 30 Aug 2022 18:38:46 +0200 Subject: [PATCH 24/59] Remove dynamic prefix in debug configuration providers (#19683) #19629 Co-authored-by: paulacamargo25 --- .../configuration/dynamicdebugConfigurationService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index 46937bdc65ba..4fb2e7e0c225 100644 --- a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts @@ -25,7 +25,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf const providers = []; providers.push({ - name: 'Dynamic Python: File', + name: 'Python: File', type: DebuggerTypeName, request: 'launch', program: '${file}', @@ -35,7 +35,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf const djangoManagePath = await this.getDjangoPath(folder); if (djangoManagePath) { providers.push({ - name: 'Dynamic Python: Django', + name: 'Python: Django', type: DebuggerTypeName, request: 'launch', program: `${workspaceFolderToken}${this.pathUtils.separator}${djangoManagePath}`, @@ -48,7 +48,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf const flaskPath = await this.getFlaskPath(folder); if (flaskPath) { providers.push({ - name: 'Dynamic Python: Flask', + name: 'Python: Flask', type: DebuggerTypeName, request: 'launch', module: 'flask', @@ -69,7 +69,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf .replaceAll(this.pathUtils.separator, '.') .replace('.py', ''); providers.push({ - name: 'Dynamic Python: FastAPI', + name: 'Python: FastAPI', type: DebuggerTypeName, request: 'launch', module: 'uvicorn', From 737c55f622a24b9d24fd4342f55a83581a89b50b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 31 Aug 2022 08:18:44 +1000 Subject: [PATCH 25/59] Launch python tb with the activated env vars (#19592) --- src/client/tensorBoard/tensorBoardSession.ts | 15 +++++++++------ .../tensorBoard/tensorBoardSessionProvider.ts | 6 +++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index e5bd67c94f6b..132547d6a4b7 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -27,7 +27,7 @@ import * as nls from 'vscode-nls'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { createPromiseFromCancellation } from '../common/cancellation'; import { tensorboardLauncher } from '../common/process/internal/scripts'; -import { IProcessServiceFactory, ObservableExecutionResult } from '../common/process/types'; +import { IPythonExecutionFactory, ObservableExecutionResult } from '../common/process/types'; import { IDisposableRegistry, IInstaller, @@ -94,7 +94,7 @@ export class TensorBoardSession { private readonly installer: IInstaller, private readonly interpreterService: IInterpreterService, private readonly workspaceService: IWorkspaceService, - private readonly processServiceFactory: IProcessServiceFactory, + private readonly pythonExecFactory: IPythonExecutionFactory, private readonly commandManager: ICommandManager, private readonly disposables: IDisposableRegistry, private readonly applicationShell: IApplicationShell, @@ -378,8 +378,8 @@ export class TensorBoardSession { // Times out if it hasn't started up after 1 minute. // Hold on to the process so we can kill it when the webview is closed. private async startTensorboardSession(logDir: string): Promise { - const pythonExecutable = await this.interpreterService.getActiveInterpreter(); - if (!pythonExecutable) { + const interpreter = await this.interpreterService.getActiveInterpreter(); + if (!interpreter) { return false; } @@ -395,10 +395,13 @@ export class TensorBoardSession { cancellable: true, }; - const processService = await this.processServiceFactory.create(); + const processService = await this.pythonExecFactory.createActivatedEnvironment({ + allowEnvironmentFetchExceptions: true, + interpreter, + }); const args = tensorboardLauncher([logDir]); const sessionStartStopwatch = new StopWatch(); - const observable = processService.execObservable(pythonExecutable.path, args); + const observable = processService.execObservable(args, {}); const result = await this.applicationShell.withProgress( progressOptions, diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index c71be0b782ac..1039aa1167d7 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -8,7 +8,7 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { Commands } from '../common/constants'; import { ContextKey } from '../common/contextKey'; -import { IProcessServiceFactory } from '../common/process/types'; +import { IPythonExecutionFactory } from '../common/process/types'; import { IDisposableRegistry, IInstaller, IPersistentState, IPersistentStateFactory } from '../common/types'; import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; import { IInterpreterService } from '../interpreter/contracts'; @@ -39,7 +39,7 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, ) { @@ -96,7 +96,7 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer this.installer, this.interpreterService, this.workspaceService, - this.processServiceFactory, + this.pythonExecFactory, this.commandManager, this.disposables, this.applicationShell, From e2680726f9fbe6ee8cdbbca9c0c0639362b6cdd2 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 31 Aug 2022 10:47:17 -0500 Subject: [PATCH 26/59] Update args type (#19638) Related to https://github.com/microsoft/debugpy/pull/1004. --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 02d57305c323..77679937b6bb 100644 --- a/package.json +++ b/package.json @@ -1195,11 +1195,14 @@ "properties": { "args": { "default": [], - "description": "Command line arguments passed to the program", + "description": "Command line arguments passed to the program.", "items": { "type": "string" }, - "type": "array" + "type": [ + "array", + "string" + ] }, "autoReload": { "default": {}, From 06e201eeffcd092f044a974219a0038656d3ec88 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 1 Sep 2022 22:27:31 -0700 Subject: [PATCH 27/59] Assign data science telemetry to Don (#19748) --- src/client/telemetry/index.ts | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 84d996f3c728..517871f52ce2 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1785,7 +1785,7 @@ export interface IEventNamePropertyMapping { * `selection` is one of 'yes', 'no', or 'do not ask again'. */ /* __GDPR__ - "tensorboard.launch_prompt_selection" : { "owner": "greazer" } + "tensorboard.launch_prompt_selection" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_LAUNCH_PROMPT_SELECTION]: { @@ -1802,8 +1802,8 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.session_launch" : { - "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" }, - "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" } + "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } } */ [EventName.TENSORBOARD_SESSION_LAUNCH]: { @@ -1821,8 +1821,8 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.session_daemon_startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" }, - "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" } + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } } */ [EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION]: { @@ -1838,7 +1838,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.session_e2e_startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" } + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } } */ [EventName.TENSORBOARD_SESSION_E2E_STARTUP_DURATION]: never | undefined; @@ -1849,7 +1849,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.session_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" } + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } } */ [EventName.TENSORBOARD_SESSION_DURATION]: never | undefined; @@ -1866,8 +1866,8 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.entrypoint_shown" : { - "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" }, - "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" } + "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } } */ [EventName.TENSORBOARD_ENTRYPOINT_SHOWN]: { @@ -1879,7 +1879,7 @@ export interface IEventNamePropertyMapping { * dependencies for launching an integrated TensorBoard session. */ /* __GDPR__ - "tensorboard.session_duration" : { "owner": "greazer" } + "tensorboard.session_duration" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_INSTALL_PROMPT_SHOWN]: never | undefined; /** @@ -1889,8 +1889,8 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.install_prompt_selection" : { - "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" }, - "operationtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "greazer" } + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "operationtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } } */ [EventName.TENSORBOARD_INSTALL_PROMPT_SELECTION]: { @@ -1901,7 +1901,7 @@ export interface IEventNamePropertyMapping { * Telemetry event sent when we find an active integrated terminal running tensorboard. */ /* __GDPR__ - "tensorboard_detected_in_integrated_terminal" : { "owner": "greazer" } + "tensorboard_detected_in_integrated_terminal" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; /** @@ -1911,10 +1911,10 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "tensorboard.package_install_result" : { - "wasprofilerpluginattempted" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "greazer" }, - "wastensorboardattempted" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "greazer" }, - "wasprofilerplugininstalled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "greazer" }, - "wastensorboardinstalled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "greazer" } + "wasprofilerpluginattempted" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" }, + "wastensorboardattempted" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" }, + "wasprofilerplugininstalled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" }, + "wastensorboardinstalled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "donjayamanne" } } */ @@ -1931,7 +1931,7 @@ export interface IEventNamePropertyMapping { * `from torch import profiler`. */ /* __GDPR__ - "tensorboard.torch_profiler_import" : { "owner": "greazer" } + "tensorboard.torch_profiler_import" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_TORCH_PROFILER_IMPORT]: never | undefined; /** @@ -1940,7 +1940,7 @@ export interface IEventNamePropertyMapping { * PyTorch profiler TensorBoard plugin. */ /* __GDPR__ - "tensorboard_jump_to_source_request" : { "owner": "greazer" } + "tensorboard_jump_to_source_request" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_JUMP_TO_SOURCE_REQUEST]: never | undefined; /** @@ -1950,7 +1950,7 @@ export interface IEventNamePropertyMapping { * on the machine currently running TensorBoard. */ /* __GDPR__ - "tensorboard_jump_to_source_file_not_found" : { "owner": "greazer" } + "tensorboard_jump_to_source_file_not_found" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND]: never | undefined; /* __GDPR__ From 2f63423ab088cd0fe8680b8d1546c3eb5d24573f Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 6 Sep 2022 10:57:04 -0700 Subject: [PATCH 28/59] Merge back release to main. (#19786) --- package-lock.json | 62 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 13ab8fdf9ef3..be50eac6b1f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "vscode-languageserver": "8.0.2-next.5", "vscode-languageserver-protocol": "3.17.2-next.6", "vscode-nls": "^5.0.1", - "vscode-tas-client": "^0.1.22", + "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", "xml2js": "^0.4.19" @@ -2057,11 +2057,11 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.8" } }, "node_modules/axobject-query": { @@ -5971,9 +5971,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", "funding": [ { "type": "individual", @@ -13004,11 +13004,11 @@ } }, "node_modules/tas-client": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.1.21.tgz", - "integrity": "sha512-7UuIwOXarCYoCTrQHY5n7M+63XuwMC0sVUdbPQzxqDB9wMjIW0JF39dnp3yoJnxr4jJUVhPtvkkXZbAD0BxCcA==", + "version": "0.1.58", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.1.58.tgz", + "integrity": "sha512-fOWii4wQXuo9Zl0oXgvjBzZWzKc5MmUR6XQWX93WU2c1SaP1plPo/zvXP8kpbZ9fvegFOHdapszYqMTRq/SRtg==", "dependencies": { - "axios": "^0.21.1" + "axios": "^0.26.1" } }, "node_modules/terser": { @@ -14583,11 +14583,11 @@ } }, "node_modules/vscode-tas-client": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.22.tgz", - "integrity": "sha512-1sYH73nhiSRVQgfZkLQNJW7VzhKM9qNbCe8QyXgiKkLhH4GflDXRPAK4yy4P41jUgula+Fc9G7i5imj1dlKfaw==", + "version": "0.1.63", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz", + "integrity": "sha512-TY5TPyibzi6rNmuUB7eRVqpzLzNfQYrrIl/0/F8ukrrbzOrKVvS31hM3urE+tbaVrnT+TMYXL16GhX57vEowhA==", "dependencies": { - "tas-client": "0.1.21" + "tas-client": "0.1.58" }, "engines": { "vscode": "^1.19.1" @@ -16935,11 +16935,11 @@ "dev": true }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.8" } }, "axobject-query": { @@ -20073,9 +20073,9 @@ } }, "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "for-in": { "version": "1.0.2", @@ -25589,11 +25589,11 @@ } }, "tas-client": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.1.21.tgz", - "integrity": "sha512-7UuIwOXarCYoCTrQHY5n7M+63XuwMC0sVUdbPQzxqDB9wMjIW0JF39dnp3yoJnxr4jJUVhPtvkkXZbAD0BxCcA==", + "version": "0.1.58", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.1.58.tgz", + "integrity": "sha512-fOWii4wQXuo9Zl0oXgvjBzZWzKc5MmUR6XQWX93WU2c1SaP1plPo/zvXP8kpbZ9fvegFOHdapszYqMTRq/SRtg==", "requires": { - "axios": "^0.21.1" + "axios": "^0.26.1" } }, "terser": { @@ -26833,11 +26833,11 @@ } }, "vscode-tas-client": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.22.tgz", - "integrity": "sha512-1sYH73nhiSRVQgfZkLQNJW7VzhKM9qNbCe8QyXgiKkLhH4GflDXRPAK4yy4P41jUgula+Fc9G7i5imj1dlKfaw==", + "version": "0.1.63", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz", + "integrity": "sha512-TY5TPyibzi6rNmuUB7eRVqpzLzNfQYrrIl/0/F8ukrrbzOrKVvS31hM3urE+tbaVrnT+TMYXL16GhX57vEowhA==", "requires": { - "tas-client": "0.1.21" + "tas-client": "0.1.58" } }, "vscode-uri": { diff --git a/package.json b/package.json index 77679937b6bb..20a39d429eb9 100644 --- a/package.json +++ b/package.json @@ -1818,7 +1818,7 @@ "vscode-languageserver": "8.0.2-next.5", "vscode-languageserver-protocol": "3.17.2-next.6", "vscode-nls": "^5.0.1", - "vscode-tas-client": "^0.1.22", + "vscode-tas-client": "^0.1.63", "winreg": "^1.2.4", "xml2js": "^0.4.19", "which": "^2.0.2" From 6b8f2e29028ef462f3933352ad009ce2be809222 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:31:26 -0700 Subject: [PATCH 29/59] Add comment for call-tas-error telemetry event (#19795) This is to classify the new call-tas-error event the Python extension sends when calls to TAS fail. --- src/client/telemetry/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 517871f52ce2..326b125df1cd 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1960,4 +1960,11 @@ export interface IEventNamePropertyMapping { "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } } */ + /* __GDPR__ + "call-tas-error" : { + "owner": "luabud", + "comment": "Logs when calls to the experiment service fails", + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth"} + } + */ } From 00316f3134eb0665df9c04517105115e6321d415 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 9 Sep 2022 15:08:43 -0700 Subject: [PATCH 30/59] Ensure full refresh icon removes any environment which isn't relevant and remove hard refresh icon in quickpick (#19806) --- src/client/apiTypes.ts | 6 +- src/client/common/constants.ts | 1 - src/client/common/utils/localize.ts | 4 - .../commands/setInterpreter.ts | 19 +- src/client/interpreter/interpreterService.ts | 2 +- src/client/proposedApi.ts | 5 +- src/client/pythonEnvironments/base/locator.ts | 4 - .../locators/composite/envsCollectionCache.ts | 48 +++- .../composite/envsCollectionService.ts | 10 +- .../commands/setInterpreter.unit.test.ts | 15 +- .../envsCollectionService.unit.test.ts | 239 ++---------------- 11 files changed, 78 insertions(+), 275 deletions(-) diff --git a/src/client/apiTypes.ts b/src/client/apiTypes.ts index 847b215bce49..6361a75edb48 100644 --- a/src/client/apiTypes.ts +++ b/src/client/apiTypes.ts @@ -121,10 +121,6 @@ export interface ActiveEnvironmentChangedParams { resource?: Uri; } -export interface RefreshEnvironmentsOptions { - clearCache?: boolean; -} - export interface IProposedExtensionAPI { environment: { /** @@ -203,7 +199,7 @@ export interface IProposedExtensionAPI { * * clearCache : When true, this will clear the cache before environment refresh * is triggered. */ - refreshEnvironment(options?: RefreshEnvironmentsOptions): Promise; + refreshEnvironment(): Promise; /** * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 30ebb88c36a9..a5570b28e5da 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -85,7 +85,6 @@ export namespace Octicons { * to change the icons. */ export namespace ThemeIcons { - export const ClearAll = 'clear-all'; export const Refresh = 'refresh'; export const SpinningLoader = 'loading~spin'; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index a45c93270c5f..2928f640d1be 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -317,10 +317,6 @@ export namespace InterpreterQuickPickList { 'InterpreterQuickPickList.refreshInterpreterList', 'Refresh Interpreter list', ); - export const clearAllAndRefreshInterpreterList = localize( - 'InterpreterQuickPickList.clearAllAndRefreshInterpreterList', - 'Clear all and Refresh Interpreter list', - ); export const refreshingInterpreterList = localize( 'InterpreterQuickPickList.refreshingInterpreterList', 'Refreshing Interpreter list...', diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c918e99bffbe..1d6bd31edf29 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -80,11 +80,6 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { tooltip: InterpreterQuickPickList.refreshInterpreterList, }; - private readonly hardRefreshButton = { - iconPath: new ThemeIcon(ThemeIcons.ClearAll), - tooltip: InterpreterQuickPickList.clearAllAndRefreshInterpreterList, - }; - private readonly noPythonInstalled: ISpecialQuickPickItem = { label: `${Octicons.Error} ${InterpreterQuickPickList.noPythonInstalled}`, detail: InterpreterQuickPickList.clickForInstructions, @@ -155,16 +150,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { matchOnDescription: true, title: InterpreterQuickPickList.browsePath.openButtonLabel, customButtonSetups: [ - { - button: this.hardRefreshButton, - callback: (quickpickInput) => { - this.refreshButtonCallback(quickpickInput, true); - }, - }, { button: this.refreshButton, callback: (quickpickInput) => { - this.refreshButtonCallback(quickpickInput, false); + this.refreshButtonCallback(quickpickInput); }, }, ], @@ -418,7 +407,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { } } - private refreshButtonCallback(input: QuickPick, clearCache: boolean) { + private refreshButtonCallback(input: QuickPick) { input.buttons = [ { iconPath: new ThemeIcon(ThemeIcons.SpinningLoader), @@ -426,9 +415,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { }, ]; this.interpreterService - .triggerRefresh(undefined, { clearCache }) + .triggerRefresh() .finally(() => { - input.buttons = [this.hardRefreshButton, this.refreshButton]; + input.buttons = [this.refreshButton]; }) .ignoreErrors(); } diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 1f563125162d..cc4bf786dd6d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -244,7 +244,7 @@ export class InterpreterService implements Disposable, IInterpreterService { traceLog('Conda envs without Python are known to not work well; fixing conda environment...'); const promise = installer.install(Product.python, await this.getInterpreterDetails(pythonPath)); shell.withProgress(progressOptions, () => promise); - promise.then(() => this.triggerRefresh(undefined, { clearCache: true }).ignoreErrors()); + promise.then(() => this.triggerRefresh().ignoreErrors()); } } } diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index e4ac6fd83caa..fc432efeb821 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -8,7 +8,6 @@ import { EnvironmentDetailsOptions, EnvironmentsChangedParams, IProposedExtensionAPI, - RefreshEnvironmentsOptions, } from './apiTypes'; import { arePathsSame } from './common/platform/fs-paths'; import { IInterpreterPathService, Resource } from './common/types'; @@ -101,8 +100,8 @@ export function buildProposedApi( setActiveEnvironment(path: string, resource?: Resource): Promise { return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - async refreshEnvironment(options?: RefreshEnvironmentsOptions) { - await discoveryApi.triggerRefresh(undefined, options ? { clearCache: options.clearCache } : undefined); + async refreshEnvironment() { + await discoveryApi.triggerRefresh(); const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); return Promise.resolve(paths); }, diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index 609010501d63..c0d1cd23991c 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -194,10 +194,6 @@ export interface GetRefreshEnvironmentsOptions { } export type TriggerRefreshOptions = { - /** - * Trigger a fresh refresh. - */ - clearCache?: boolean; /** * Only trigger a refresh if it hasn't already been triggered for this session. */ diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index 7c12faf524c4..14663e2d117d 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Event } from 'vscode'; +import { isTestExecution } from '../../../../common/constants'; import { traceInfo } from '../../../../logging'; import { reportInterpretersChanged } from '../../../../proposedApi'; import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; @@ -52,13 +53,9 @@ export interface IEnvsCollectionCache { /** * Removes invalid envs from cache. Note this does not check for outdated info when * validating cache. + * @param latestListOfEnvs Carries list of latest envs for this workspace session if known. */ - validateCache(): Promise; - - /** - * Clears the in-memory cache. This can be used for hard refresh. - */ - clearCache(): Promise; + validateCache(latestListOfEnvs?: PythonEnvInfo[]): Promise; } export type PythonEnvLatestInfo = { hasLatestInfo?: boolean } & PythonEnvInfo; @@ -79,15 +76,35 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { + public async validateCache(latestListOfEnvs?: PythonEnvInfo[]): Promise { /** * We do check if an env has updated as we already run discovery in background * which means env cache will have up-to-date envs eventually. This also means - * we avoid the cost of running lstat. So simply remove envs which no longer - * exist. + * we avoid the cost of running lstat. So simply remove envs which are no longer + * valid. */ const areEnvsValid = await Promise.all( - this.envs.map((e) => (e.executable.filename === 'python' ? true : pathExists(e.executable.filename))), + this.envs.map(async (cachedEnv) => { + const { path } = getEnvPath(cachedEnv.executable.filename, cachedEnv.location); + if (await pathExists(path)) { + if (latestListOfEnvs) { + /** + * Only consider a cached env to be valid if it's relevant. That means: + * * It is either reported in the latest complete refresh for this session. + * * Or it is relevant for some other workspace folder which is not opened currently. + */ + if (cachedEnv.searchLocation) { + return true; + } + if (latestListOfEnvs.some((env) => cachedEnv.id === env.id)) { + return true; + } + } else { + return true; + } + } + return false; + }), ); const invalidIndexes = areEnvsValid .map((isValid, index) => (isValid ? -1 : index)) @@ -194,6 +211,15 @@ async function validateInfo(env: PythonEnvInfo) { export async function createCollectionCache(storage: IPersistentStorage): Promise { const cache = new PythonEnvInfoCache(storage); await cache.clearAndReloadFromStorage(); - await cache.validateCache(); + await validateCache(cache); return cache; } + +async function validateCache(cache: PythonEnvInfoCache) { + if (isTestExecution()) { + // For purposes for test execution, block on validation so that we can determinally know when it finishes. + return cache.validateCache(); + } + // Validate in background so it doesn't block on returning the API object. + return cache.validateCache().ignoreErrors(); +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index f652b420ecf8..0e1466bc385d 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -108,15 +108,12 @@ export class EnvsCollectionService extends PythonEnvsWatcher this.sendTelemetry(query, stopWatch)); } - private startRefresh(query: PythonLocatorQuery | undefined, options?: TriggerRefreshOptions): Promise { - if (options?.clearCache) { - this.cache.clearCache(); - } + private startRefresh(query: PythonLocatorQuery | undefined): Promise { this.createProgressStates(query); const promise = this.addEnvsToCacheForQuery(query); return promise @@ -176,7 +173,8 @@ export class EnvsCollectionService extends PythonEnvsWatcher { workspace = TypeMoq.Mock.ofType(); interpreterService = mock(); when(interpreterService.refreshPromise).thenReturn(undefined); + when(interpreterService.triggerRefresh()).thenResolve(); when(interpreterService.triggerRefresh(anything(), anything())).thenResolve(); workspace.setup((w) => w.rootPath).returns(() => 'rootPath'); @@ -568,17 +569,11 @@ suite('Set Interpreter Command', () => { expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); const refreshButtons = actualParameters!.customButtonSetups; expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + expect(refreshButtons?.length).to.equal(1); - expect(refreshButtons?.length).to.equal(2); - let arg; - when(interpreterService.triggerRefresh(undefined, anything())).thenCall((_, _arg) => { - arg = _arg; - return Promise.resolve(); - }); await refreshButtons![0].callback!({} as QuickPick); // Invoke callback, meaning that the refresh button is clicked. - expect(arg).to.deep.equal({ clearCache: true }); - await refreshButtons![1].callback!({} as QuickPick); // Invoke callback, meaning that the refresh button is clicked. - expect(arg).to.deep.equal({ clearCache: false }); + + verify(interpreterService.triggerRefresh()).once(); }); test('Events to update quickpick updates the quickpick accordingly', async () => { diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts index 1dd31c4424e5..7bb70bead0a9 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -54,8 +54,8 @@ suite('Python envs locator - Environments Collection', async () => { return envs; } - function createEnv(executable: string, searchLocation?: Uri, name?: string) { - return buildEnvInfo({ executable, searchLocation, name }); + function createEnv(executable: string, searchLocation?: Uri, name?: string, location?: string) { + return buildEnvInfo({ executable, searchLocation, name, location }); } function getLocatorEnvs() { @@ -72,14 +72,23 @@ suite('Python envs locator - Environments Collection', async () => { } function getValidCachedEnvs() { + const cachedEnvForWorkspace = createEnv( + path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1', 'win1', 'python.exe'), + Uri.file(path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1')), + ); const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps'); const envCached1 = createEnv(path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', 'python.exe')); const envCached2 = createEnv( path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), Uri.file(TEST_LAYOUT_ROOT), ); - const envCached3 = createEnv('python'); - return [envCached1, envCached2, envCached3]; + const envCached3 = createEnv( + 'python', + undefined, + undefined, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython'), + ); + return [cachedEnvForWorkspace, envCached1, envCached2, envCached3]; } function getCachedEnvs() { @@ -87,10 +96,11 @@ suite('Python envs locator - Environments Collection', async () => { return [...getValidCachedEnvs(), envCached3]; } - function getExpectedEnvs(doNotIncludeCached?: boolean) { - const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps'); - const envCached1 = createEnv(path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', 'python.exe')); - const envCached2 = createEnv('python'); + function getExpectedEnvs() { + const cachedEnvForWorkspace = createEnv( + path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1', 'win1', 'python.exe'), + Uri.file(path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1')), + ); const env1 = createEnv(path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), undefined, updatedName); const env2 = createEnv( path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), @@ -102,13 +112,8 @@ suite('Python envs locator - Environments Collection', async () => { undefined, updatedName, ); - if (doNotIncludeCached) { - return [env1, env2, env3].map((e: PythonEnvLatestInfo) => { - e.hasLatestInfo = true; - return e; - }); - } - return [envCached1, envCached2, env1, env2, env3].map((e: PythonEnvLatestInfo) => { + // Do not include cached envs which were not yielded by the locator, unless it belongs to some workspace. + return [cachedEnvForWorkspace, env1, env2, env3].map((e: PythonEnvLatestInfo) => { e.hasLatestInfo = true; return e; }); @@ -145,99 +150,6 @@ suite('Python envs locator - Environments Collection', async () => { ); }); - test('triggerRefresh() refreshes the collection and storage with any new environments', async () => { - const onUpdated = new EventEmitter(); - const locatedEnvs = getLocatorEnvs(); - const parentLocator = new SimpleLocator(locatedEnvs, { - onUpdated: onUpdated.event, - after: async () => { - locatedEnvs.forEach((env, index) => { - const update = cloneDeep(env); - update.name = updatedName; - onUpdated.fire({ index, update }); - }); - onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); - // It turns out the last env is invalid, ensure it does not appear in the final result. - onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); - }, - }); - const cache = await createCollectionCache({ - load: async () => getCachedEnvs(), - store: async (e) => { - storage = e; - }, - }); - collectionService = new EnvsCollectionService(cache, parentLocator); - - await collectionService.triggerRefresh(); - const envs = collectionService.getEnvs(); - - const expected = getExpectedEnvs(); - assertEnvsEqual(envs, expected); - assertEnvsEqual(storage, expected); - - const eventData = [ - { - path: path.join(TEST_LAYOUT_ROOT, 'doesNotExist'), - type: 'remove', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), - type: 'add', - }, - { - path: path.join( - TEST_LAYOUT_ROOT, - 'pyenv2', - '.pyenv', - 'pyenv-win', - 'versions', - '3.6.9', - 'bin', - 'python.exe', - ), - type: 'add', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'add', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), - type: 'update', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), - type: 'update', - }, - { - path: path.join( - TEST_LAYOUT_ROOT, - 'pyenv2', - '.pyenv', - 'pyenv-win', - 'versions', - '3.6.9', - 'bin', - 'python.exe', - ), - type: 'update', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'update', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'remove', - }, - ]; - eventData.forEach((d) => { - sinon.assert.calledWithExactly(reportInterpretersChangedStub, [d]); - }); - sinon.assert.callCount(reportInterpretersChangedStub, eventData.length); - }); - test('If `ifNotTriggerredAlready` option is set and a refresh for query is already triggered, triggerRefresh() does not trigger a refresh', async () => { const onUpdated = new EventEmitter(); const locatedEnvs = getLocatorEnvs(); @@ -312,70 +224,9 @@ suite('Python envs locator - Environments Collection', async () => { }); const expected = getExpectedEnvs(); assertEnvsEqual(envs, expected); - - const eventData = [ - { - path: path.join(TEST_LAYOUT_ROOT, 'doesNotExist'), - type: 'remove', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), - type: 'add', - }, - { - path: path.join( - TEST_LAYOUT_ROOT, - 'pyenv2', - '.pyenv', - 'pyenv-win', - 'versions', - '3.6.9', - 'bin', - 'python.exe', - ), - type: 'add', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'add', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), - type: 'update', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), - type: 'update', - }, - { - path: path.join( - TEST_LAYOUT_ROOT, - 'pyenv2', - '.pyenv', - 'pyenv-win', - 'versions', - '3.6.9', - 'bin', - 'python.exe', - ), - type: 'update', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'update', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'remove', - }, - ]; - eventData.forEach((d) => { - sinon.assert.calledWithExactly(reportInterpretersChangedStub, [d]); - }); - sinon.assert.callCount(reportInterpretersChangedStub, eventData.length); }); - test('If `clearCache` option is set triggerRefresh() clears the cache before refreshing and fires expected events', async () => { + test('triggerRefresh() refreshes the collection with any new envs & removes cached envs if not relevant', async () => { const onUpdated = new EventEmitter(); const locatedEnvs = getLocatorEnvs(); const cachedEnvs = getCachedEnvs(); @@ -405,17 +256,18 @@ suite('Python envs locator - Environments Collection', async () => { events.push(e); }); - await collectionService.triggerRefresh(undefined, { clearCache: true }); + await collectionService.triggerRefresh(); let envs = cachedEnvs; // Ensure when all the events are applied to the original list in sequence, the final list is as expected. events.forEach((e) => { envs = applyChangeEventToEnvList(envs, e); }); - const expected = getExpectedEnvs(true); + const expected = getExpectedEnvs(); assertEnvsEqual(envs, expected); const queriedEnvs = collectionService.getEnvs(); assertEnvsEqual(queriedEnvs, expected); + assertEnvsEqual(storage, expected); }); test('Ensure progress stage updates are emitted correctly and refresh promises correct track promise for each stage', async () => { @@ -502,49 +354,6 @@ suite('Python envs locator - Environments Collection', async () => { expect(refreshPromise).to.equal(undefined, 'All paths discovered stage not applicable if a query is provided'); }); - test('refreshPromise() correctly indicates the status of the refresh', async () => { - const parentLocator = new SimpleLocator(getLocatorEnvs()); - const cache = await createCollectionCache({ - load: async () => getCachedEnvs(), - store: async () => noop(), - }); - collectionService = new EnvsCollectionService(cache, parentLocator); - - await collectionService.triggerRefresh(); - - const eventData = [ - { - path: path.join(TEST_LAYOUT_ROOT, 'doesNotExist'), - type: 'remove', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), - type: 'add', - }, - { - path: path.join( - TEST_LAYOUT_ROOT, - 'pyenv2', - '.pyenv', - 'pyenv-win', - 'versions', - '3.6.9', - 'bin', - 'python.exe', - ), - type: 'add', - }, - { - path: path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe'), - type: 'add', - }, - ]; - eventData.forEach((d) => { - sinon.assert.calledWithExactly(reportInterpretersChangedStub, [d]); - }); - sinon.assert.callCount(reportInterpretersChangedStub, eventData.length); - }); - test('resolveEnv() uses cache if complete and up to date info is available', async () => { const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); const cachedEnvs = getCachedEnvs(); From 5cc9092be59f258851956ef078007219260742a0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 12 Sep 2022 17:07:57 -0700 Subject: [PATCH 31/59] Revert to using `conda activate` for fetching activated environment variables (#19819) Because of https://github.com/conda/conda/issues/11814, `conda activate` is faster. Closes https://github.com/microsoft/vscode-python/issues/19347 For https://github.com/microsoft/vscode-python/issues/19101 --- src/client/interpreter/activation/service.ts | 66 +++++++------------ .../activation/service.unit.test.ts | 7 +- 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 24007581daf5..baa05656853a 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -16,7 +16,7 @@ import { sleep } from '../../common/utils/async'; import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; @@ -30,7 +30,6 @@ import { traceVerbose, traceWarn, } from '../../logging'; -import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const CACHE_DURATION = 10 * 60 * 1000; @@ -170,41 +169,20 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi if (!shellInfo) { return; } + let isPossiblyCondaEnv = false; try { - let command: string | undefined; - let [args, parse] = internalScripts.printEnvVariables(); - args.forEach((arg, i) => { - args[i] = arg.toCommandArgumentForPythonExt(); - }); - interpreter = interpreter ?? (await this.interpreterService.getActiveInterpreter(resource)); - if (interpreter?.envType === EnvironmentType.Conda) { - const conda = await Conda.getConda(); - const pythonArgv = await conda?.getRunPythonArgs({ - name: interpreter.envName, - prefix: interpreter.envPath ?? '', - }); - if (pythonArgv) { - // Using environment prefix isn't needed as the marker script already takes care of it. - command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); - } - } - if (!command) { - const activationCommands = await this.helper.getEnvironmentActivationShellCommands( - resource, - shellInfo.shellType, - interpreter, - ); - traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); - if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { - return; - } - // Run the activate command collect the environment from it. - const activationCommand = this.fixActivationCommands(activationCommands).join(' && '); - // In order to make sure we know where the environment output is, - // put in a dummy echo we can look for - command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`; + const activationCommands = await this.helper.getEnvironmentActivationShellCommands( + resource, + shellInfo.shellType, + interpreter, + ); + traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); + if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { + return; } - + isPossiblyCondaEnv = activationCommands.join(' ').toLowerCase().includes('conda'); + // Run the activate command collect the environment from it. + const activationCommand = this.fixActivationCommands(activationCommands).join(' && '); const processService = await this.processServiceFactory.create(resource); const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); const hasCustomEnvVars = Object.keys(customEnvVars).length; @@ -216,6 +194,14 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi env[PYTHON_WARNINGS] = 'ignore'; traceVerbose(`${hasCustomEnvVars ? 'Has' : 'No'} Custom Env Vars`); + + // In order to make sure we know where the environment output is, + // put in a dummy echo we can look for + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + const command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`; traceVerbose(`Activating Environment to capture Environment variables, ${command}`); // Do some wrapping of the call. For two reasons: @@ -233,10 +219,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi result = await processService.shellExec(command, { env, shell: shellInfo.shell, - timeout: - interpreter?.envType === EnvironmentType.Conda - ? CONDA_ENVIRONMENT_TIMEOUT - : ENVIRONMENT_TIMEOUT, + timeout: isPossiblyCondaEnv ? CONDA_ENVIRONMENT_TIMEOUT : ENVIRONMENT_TIMEOUT, maxBuffer: 1000 * 1000, throwOnStdErr: false, }); @@ -282,7 +265,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } catch (e) { traceError('getActivatedEnvironmentVariables', e); sendTelemetryEvent(EventName.ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED, undefined, { - isPossiblyCondaEnv: interpreter?.envType === EnvironmentType.Conda, + isPossiblyCondaEnv, terminal: shellInfo.shellType, }); @@ -300,9 +283,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi @traceDecoratorError('Failed to parse Environment variables') @traceDecoratorVerbose('parseEnvironmentOutput', TraceOptions.None) protected parseEnvironmentOutput(output: string, parse: (out: string) => NodeJS.ProcessEnv | undefined) { - if (output.indexOf(ENVIRONMENT_PREFIX) === -1) { - return parse(output); - } output = output.substring(output.indexOf(ENVIRONMENT_PREFIX) + ENVIRONMENT_PREFIX.length); const js = output.substring(output.indexOf('{')).trim(); return parse(js); diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index d50b2b5d5995..9e705f247ac9 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -58,7 +58,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { architecture: Architecture.x64, }; - function initSetup(interpreter: PythonEnvironment | undefined) { + function initSetup() { helper = mock(TerminalHelper); platform = mock(PlatformService); processServiceFactory = mock(ProcessServiceFactory); @@ -71,7 +71,6 @@ suite('Interpreters Activation - Python Environment Variables', () => { onDidChangeInterpreter = new EventEmitter(); when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event); when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); service = new EnvironmentActivationService( instance(helper), instance(platform), @@ -90,7 +89,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { [undefined, Uri.parse('a')].forEach((resource) => [undefined, pythonInterpreter].forEach((interpreter) => { suite(title(resource, interpreter), () => { - setup(() => initSetup(interpreter)); + setup(initSetup); test('Unknown os will return empty variables', async () => { when(platform.osType).thenReturn(OSType.Unknown); const env = await service.getActivatedEnvironmentVariables(resource); @@ -103,7 +102,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { osTypes.forEach((osType) => { suite(osType.name, () => { - setup(() => initSetup(interpreter)); + setup(initSetup); test('getEnvironmentActivationShellCommands will be invoked', async () => { when(platform.osType).thenReturn(osType.value); when( From 2ef2db46ffff6694e40269db10ada2c511e99a86 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 13 Sep 2022 16:22:10 -0700 Subject: [PATCH 32/59] Ensure interpreter quickpick is initialized synchronously (#19828) For https://github.com/microsoft/vscode-python/issues/19101 Before the change item events start coming in, we have to ensure quickpick is ready to receive those events. --- src/client/common/utils/multiStepInput.ts | 75 ++++++++++--------- .../commands/setInterpreter.ts | 2 +- .../commands/setInterpreter.unit.test.ts | 12 ++- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index 1cb6bad11d66..daac8574227d 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -46,7 +46,7 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T; + activeItem?: T | Promise; placeholder: string; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; @@ -127,29 +127,45 @@ export class MultiStepInput implements IMultiStepInput { initialize, }: P): Promise> { const disposables: Disposable[] = []; + const input = this.shell.createQuickPick(); + input.title = title; + input.step = step; + input.sortByLabel = sortByLabel || false; + input.totalSteps = totalSteps; + input.placeholder = placeholder; + input.ignoreFocusOut = true; + input.items = items; + input.matchOnDescription = matchOnDescription || false; + input.matchOnDetail = matchOnDetail || false; + input.buttons = this.steps.length > 1 ? [QuickInputButtons.Back] : []; + if (customButtonSetups) { + for (const customButtonSetup of customButtonSetups) { + input.buttons = [...input.buttons, customButtonSetup.button]; + } + } + if (this.current) { + this.current.dispose(); + } + this.current = input; + if (onChangeItem) { + disposables.push(onChangeItem.event((e) => onChangeItem.callback(e, input))); + } + // Quickpick should be initialized synchronously and on changed item handlers are registered synchronously. + if (initialize) { + initialize(); + } + if (activeItem) { + input.activeItems = [await activeItem]; + } else { + input.activeItems = []; + } + this.current.show(); + // Keep scroll position is only meant to keep scroll position when updating items, + // so do it after initialization. This ensures quickpick starts with the active + // item in focus when this is true, instead of having scroll position at top. + input.keepScrollPosition = keepScrollPosition; try { return await new Promise>((resolve, reject) => { - const input = this.shell.createQuickPick(); - input.title = title; - input.step = step; - input.sortByLabel = sortByLabel || false; - input.totalSteps = totalSteps; - input.placeholder = placeholder; - input.ignoreFocusOut = true; - input.items = items; - input.matchOnDescription = matchOnDescription || false; - input.matchOnDetail = matchOnDetail || false; - if (activeItem) { - input.activeItems = [activeItem]; - } else { - input.activeItems = []; - } - input.buttons = this.steps.length > 1 ? [QuickInputButtons.Back] : []; - if (customButtonSetups) { - for (const customButtonSetup of customButtonSetups) { - input.buttons = [...input.buttons, customButtonSetup.button]; - } - } disposables.push( input.onDidTriggerButton(async (item) => { if (item === QuickInputButtons.Back) { @@ -176,21 +192,6 @@ export class MultiStepInput implements IMultiStepInput { }), ); } - if (this.current) { - this.current.dispose(); - } - this.current = input; - if (onChangeItem) { - disposables.push(onChangeItem.event((e) => onChangeItem.callback(e, input))); - } - this.current.show(); - if (initialize) { - initialize(); - } - // Keep scroll position is only meant to keep scroll position when updating items, - // so do it after initialization. This ensures quickpick starts with the active - // item in focus when this is true, instead of having scroll position at top. - input.keepScrollPosition = keepScrollPosition; }); } finally { disposables.forEach((d) => d.dispose()); diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 1d6bd31edf29..aad89c58959b 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -145,7 +145,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, - activeItem: await this.getActiveItem(state.workspace, suggestions), + activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, title: InterpreterQuickPickList.browsePath.openButtonLabel, diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index be94c5f456dc..1bd27b5d2cbb 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -244,7 +244,6 @@ suite('Set Interpreter Command', () => { const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, - activeItem: recommended, matchOnDetail: true, matchOnDescription: true, title: InterpreterQuickPickList.browsePath.openButtonLabel, @@ -267,6 +266,9 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; + const activeItem = await actualParameters!.activeItem; + assert.deepStrictEqual(activeItem, recommended); + delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -281,7 +283,6 @@ suite('Set Interpreter Command', () => { const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, // Verify suggestions - activeItem: noPythonInstalled, // Verify active item matchOnDetail: true, matchOnDescription: true, title: InterpreterQuickPickList.browsePath.openButtonLabel, @@ -308,6 +309,9 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; + const activeItem = await actualParameters!.activeItem; + assert.deepStrictEqual(activeItem, noPythonInstalled); + delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -525,7 +529,6 @@ suite('Set Interpreter Command', () => { const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, - activeItem: recommended, matchOnDetail: true, matchOnDescription: true, title: InterpreterQuickPickList.browsePath.openButtonLabel, @@ -549,6 +552,9 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; + const activeItem = await actualParameters!.activeItem; + assert.deepStrictEqual(activeItem, recommended); + delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); From 2f4e9a061f21797e96f45595666450e0f581a398 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 13 Sep 2022 16:25:21 -0700 Subject: [PATCH 33/59] Ensure we start watching environments when activating discovery component (#19827) Ensure we start watchers and create workspace related objects when activating discovery component --- .../locators/common/resourceBasedLocator.ts | 24 +++++++++++++++---- .../locators/lowLevel/fsWatchingLocator.ts | 1 + .../base/locators/wrappers.ts | 1 + 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts index 451df3fe38a1..2a524ad7d28f 100644 --- a/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts +++ b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts @@ -4,6 +4,7 @@ import { IDisposable } from '../../../../common/types'; import { createDeferred, Deferred } from '../../../../common/utils/async'; import { Disposables } from '../../../../common/utils/resourceLifecycle'; +import { traceError } from '../../../../logging'; import { PythonEnvInfo } from '../../info'; import { IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../locator'; @@ -28,15 +29,22 @@ export abstract class LazyResourceBasedLocator extends Locato private watchersReady?: Deferred; + /** + * This can be used to initialize resources when subclasses are created. + */ + protected async activate(): Promise { + await this.ensureResourcesReady(); + // There is not need to wait for the watchers to get started. + this.ensureWatchersReady().ignoreErrors(); + } + public async dispose(): Promise { await this.disposables.dispose(); } public async *iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { - await this.ensureResourcesReady(); + await this.activate(); yield* this.doIterEnvs(query); - // There is not need to wait for the watchers to get started. - this.ensureWatchersReady().ignoreErrors(); } /** @@ -87,7 +95,10 @@ export abstract class LazyResourceBasedLocator extends Locato return; } this.resourcesReady = createDeferred(); - await this.initResources(); + await this.initResources().catch((ex) => { + traceError(ex); + this.resourcesReady?.reject(ex); + }); this.resourcesReady.resolve(); } @@ -97,7 +108,10 @@ export abstract class LazyResourceBasedLocator extends Locato return; } this.watchersReady = createDeferred(); - await this.initWatchers(); + await this.initWatchers().catch((ex) => { + traceError(ex); + this.watchersReady?.reject(ex); + }); this.watchersReady.resolve(); } } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts index df9275c0eee4..bccc705aa818 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -84,6 +84,7 @@ export abstract class FSWatchingLocator extends LazyResourceB private readonly watcherKind: FSWatcherKind = FSWatcherKind.Global, ) { super(); + this.activate().ignoreErrors(); } protected async initWatchers(): Promise { diff --git a/src/client/pythonEnvironments/base/locators/wrappers.ts b/src/client/pythonEnvironments/base/locators/wrappers.ts index 9313ade20218..fbc21fb44b21 100644 --- a/src/client/pythonEnvironments/base/locators/wrappers.ts +++ b/src/client/pythonEnvironments/base/locators/wrappers.ts @@ -59,6 +59,7 @@ export class WorkspaceLocators extends LazyResourceBasedLocat constructor(private readonly watchRoots: WatchRootsFunc, private readonly factories: WorkspaceLocatorFactory[]) { super(); + this.activate().ignoreErrors(); } public async dispose(): Promise { From fc45b1c604cd30c27039bf2addf02949ccaf5bba Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 13 Sep 2022 17:31:24 -0700 Subject: [PATCH 34/59] Ensure an environment is only reported after the final type of environment is known (#19821) For https://github.com/microsoft/vscode-python/issues/19101 --- package.nls.json | 2 +- .../base/locators/composite/envsResolver.ts | 15 ++++++++++++++- .../locators/composite/envsResolver.unit.test.ts | 8 ++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/package.nls.json b/package.nls.json index 54cb7a7fd9a4..611c98ed85e2 100644 --- a/package.nls.json +++ b/package.nls.json @@ -19,7 +19,7 @@ "python.command.python.enableLinting.title": "Enable/Disable Linting", "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", - "python.command.python.clearCacheAndReload.title": "Clear Internal Cache and Reload Window", + "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index 407d2fe12172..1baa3f36c993 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -5,7 +5,7 @@ import { cloneDeep } from 'lodash'; import { Event, EventEmitter } from 'vscode'; import { identifyEnvironment } from '../../../common/environmentIdentifier'; import { IEnvironmentInfoService } from '../../info/environmentInfoService'; -import { PythonEnvInfo } from '../../info'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; import { getEnvPath, setEnvDisplayString } from '../../info/env'; import { InterpreterInformation } from '../../info/interpreter'; import { @@ -63,6 +63,7 @@ export class PythonEnvsResolver implements IResolvingLocator { iterator: IPythonEnvsIterator, didUpdate: EventEmitter, ): IPythonEnvsIterator { + const environmentKinds = new Map(); const state = { done: false, pending: 0, @@ -86,6 +87,7 @@ export class PythonEnvsResolver implements IResolvingLocator { ); } else if (seen[event.index] !== undefined) { const old = seen[event.index]; + await setKind(event.update, environmentKinds); seen[event.index] = await resolveBasicEnv(event.update, true); didUpdate.fire({ old, index: event.index, update: seen[event.index] }); this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors(); @@ -103,6 +105,7 @@ export class PythonEnvsResolver implements IResolvingLocator { let result = await iterator.next(); while (!result.done) { // Use cache from the current refresh where possible. + await setKind(result.value, environmentKinds); const currEnv = await resolveBasicEnv(result.value, true); seen.push(currEnv); yield currEnv; @@ -139,6 +142,16 @@ export class PythonEnvsResolver implements IResolvingLocator { } } +async function setKind(env: BasicEnvInfo, environmentKinds: Map) { + const { path } = getEnvPath(env.executablePath, env.envPath); + let kind = environmentKinds.get(path); + if (!kind) { + kind = await identifyEnvironment(path); + environmentKinds.set(path, kind); + } + env.kind = kind; +} + /** * When all info from incoming iterator has been received and all background calls finishes, notify that we're done * @param state Carries the current state of progress diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts index 6cd6d53330a5..158dab04e875 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -193,16 +193,16 @@ suite('Python envs locator - Environments Resolver', () => { test('Updates to environments from the incoming iterator are applied properly', async () => { // Arrange const env = createBasicEnv( - PythonEnvKind.Venv, + PythonEnvKind.Unknown, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), ); const updatedEnv = createBasicEnv( - PythonEnvKind.Poetry, + PythonEnvKind.VirtualEnv, // Ensure this type is discarded. path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), ); const resolvedUpdatedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), - PythonEnvKind.Poetry, + PythonEnvKind.Venv, undefined, 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), @@ -225,7 +225,7 @@ suite('Python envs locator - Environments Resolver', () => { // Assert assertEnvsEqual(envs, [ - createExpectedEnvInfo(resolvedUpdatedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': poetry)"), + createExpectedEnvInfo(resolvedUpdatedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"), ]); didUpdate.dispose(); }); From 8e913329b3698c2f486e3d0fab643cc7c6621e5c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 14 Sep 2022 13:55:32 -0700 Subject: [PATCH 35/59] Simplify buffer decoder. (#19836) --- src/client/common/process/decoder.ts | 11 +++----- src/client/common/process/proc.ts | 15 +++-------- src/client/common/process/processFactory.ts | 5 ++-- .../common/process/pythonExecutionFactory.ts | 4 +-- src/client/common/process/rawProcessApis.ts | 19 ++++--------- src/client/common/process/serviceRegistry.ts | 4 +-- src/client/common/process/types.ts | 5 ---- src/test/common.ts | 3 +-- src/test/common/process/decoder.test.ts | 10 +++---- src/test/common/process/proc.exec.test.ts | 27 +++++++++---------- .../common/process/proc.observable.test.ts | 17 ++++++------ .../process/processFactory.unit.test.ts | 6 +---- .../pythonExecutionFactory.unit.test.ts | 5 ---- .../process/serviceRegistry.unit.test.ts | 3 --- src/test/linters/lint.functional.test.ts | 9 ------- src/test/serviceRegistry.ts | 5 +--- .../terminals/codeExecution/helper.test.ts | 3 +-- 17 files changed, 45 insertions(+), 106 deletions(-) diff --git a/src/client/common/process/decoder.ts b/src/client/common/process/decoder.ts index 4e03b48501d0..76cc7a349816 100644 --- a/src/client/common/process/decoder.ts +++ b/src/client/common/process/decoder.ts @@ -2,14 +2,9 @@ // Licensed under the MIT License. import * as iconv from 'iconv-lite'; -import { injectable } from 'inversify'; import { DEFAULT_ENCODING } from './constants'; -import { IBufferDecoder } from './types'; -@injectable() -export class BufferDecoder implements IBufferDecoder { - public decode(buffers: Buffer[], encoding: string = DEFAULT_ENCODING): string { - encoding = iconv.encodingExists(encoding) ? encoding : DEFAULT_ENCODING; - return iconv.decode(Buffer.concat(buffers), encoding); - } +export function decodeBuffer(buffers: Buffer[], encoding: string = DEFAULT_ENCODING): string { + encoding = iconv.encodingExists(encoding) ? encoding : DEFAULT_ENCODING; + return iconv.decode(Buffer.concat(buffers), encoding); } diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index 22795cd2b20f..0ac610e3eac9 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -6,19 +6,12 @@ import { traceError } from '../../logging'; import { IDisposable } from '../types'; import { EnvironmentVariables } from '../variables/types'; import { execObservable, killPid, plainExec, shellExec } from './rawProcessApis'; -import { - ExecutionResult, - IBufferDecoder, - IProcessService, - ObservableExecutionResult, - ShellOptions, - SpawnOptions, -} from './types'; +import { ExecutionResult, IProcessService, ObservableExecutionResult, ShellOptions, SpawnOptions } from './types'; export class ProcessService extends EventEmitter implements IProcessService { private processesToKill = new Set(); - constructor(private readonly decoder: IBufferDecoder, private readonly env?: EnvironmentVariables) { + constructor(private readonly env?: EnvironmentVariables) { super(); } @@ -47,13 +40,13 @@ export class ProcessService extends EventEmitter implements IProcessService { } public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { - const result = execObservable(file, args, options, this.decoder, this.env, this.processesToKill); + const result = execObservable(file, args, options, this.env, this.processesToKill); this.emit('exec', file, args, options); return result; } public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { - const promise = plainExec(file, args, options, this.decoder, this.env, this.processesToKill); + const promise = plainExec(file, args, options, this.env, this.processesToKill); this.emit('exec', file, args, options); return promise; } diff --git a/src/client/common/process/processFactory.ts b/src/client/common/process/processFactory.ts index 13bf4f09a250..8681d5073d8e 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -8,19 +8,18 @@ import { Uri } from 'vscode'; import { IDisposableRegistry } from '../types'; import { IEnvironmentVariablesProvider } from '../variables/types'; import { ProcessService } from './proc'; -import { IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory } from './types'; +import { IProcessLogger, IProcessService, IProcessServiceFactory } from './types'; @injectable() export class ProcessServiceFactory implements IProcessServiceFactory { constructor( @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, @inject(IProcessLogger) private readonly processLogger: IProcessLogger, - @inject(IBufferDecoder) private readonly decoder: IBufferDecoder, @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, ) {} public async create(resource?: Uri): Promise { const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); - const proc: IProcessService = new ProcessService(this.decoder, customEnvVars); + const proc: IProcessService = new ProcessService(customEnvVars); this.disposableRegistry.push(proc); return proc.on('exec', this.processLogger.logProcess.bind(this.processLogger)); } diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index 569d3595ce19..02c42beb1400 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -15,7 +15,6 @@ import { createPythonProcessService } from './pythonProcess'; import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionFactoryCreationOptions, - IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory, @@ -40,7 +39,6 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService, @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, @inject(IConfigurationService) private readonly configService: IConfigurationService, - @inject(IBufferDecoder) private readonly decoder: IBufferDecoder, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, @inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService, @inject(IInterpreterPathService) private readonly interpreterPathExpHelper: IInterpreterPathService, @@ -110,7 +108,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { const pythonPath = options.interpreter ? options.interpreter.path : this.configService.getSettings(options.resource).pythonPath; - const processService: IProcessService = new ProcessService(this.decoder, { ...envVars }); + const processService: IProcessService = new ProcessService({ ...envVars }); processService.on('exec', this.logger.logProcess.bind(this.logger)); this.disposables.push(processService); diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index b07c640c2e95..43f73e671e94 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -8,16 +8,9 @@ import { IDisposable } from '../types'; import { createDeferred } from '../utils/async'; import { EnvironmentVariables } from '../variables/types'; import { DEFAULT_ENCODING } from './constants'; -import { - ExecutionResult, - IBufferDecoder, - ObservableExecutionResult, - Output, - ShellOptions, - SpawnOptions, - StdErrError, -} from './types'; +import { ExecutionResult, ObservableExecutionResult, Output, ShellOptions, SpawnOptions, StdErrError } from './types'; import { noop } from '../utils/misc'; +import { decodeBuffer } from './decoder'; function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { const defaultOptions = { ...options }; @@ -90,7 +83,6 @@ export function plainExec( file: string, args: string[], options: SpawnOptions = {}, - decoder?: IBufferDecoder, defaultEnv?: EnvironmentVariables, disposables?: Set, ): Promise> { @@ -141,11 +133,11 @@ export function plainExec( return; } const stderr: string | undefined = - stderrBuffers.length === 0 ? undefined : decoder?.decode(stderrBuffers, encoding); + stderrBuffers.length === 0 ? undefined : decodeBuffer(stderrBuffers, encoding); if (stderr && stderr.length > 0 && options.throwOnStdErr) { deferred.reject(new StdErrError(stderr)); } else { - let stdout = decoder ? decoder.decode(stdoutBuffers, encoding) : ''; + let stdout = decodeBuffer(stdoutBuffers, encoding); stdout = filterOutputUsingCondaRunMarkers(stdout); deferred.resolve({ stdout, stderr }); } @@ -177,7 +169,6 @@ export function execObservable( file: string, args: string[], options: SpawnOptions = {}, - decoder?: IBufferDecoder, defaultEnv?: EnvironmentVariables, disposables?: Set, ): ObservableExecutionResult { @@ -220,7 +211,7 @@ export function execObservable( } const sendOutput = (source: 'stdout' | 'stderr', data: Buffer) => { - let out = decoder ? decoder.decode([data], encoding) : ''; + let out = decodeBuffer([data], encoding); if (source === 'stderr' && options.throwOnStdErr) { subscriber.error(new StdErrError(out)); } else { diff --git a/src/client/common/process/serviceRegistry.ts b/src/client/common/process/serviceRegistry.ts index 27684a20cc32..0ea57231148a 100644 --- a/src/client/common/process/serviceRegistry.ts +++ b/src/client/common/process/serviceRegistry.ts @@ -2,14 +2,12 @@ // Licensed under the MIT License. import { IServiceManager } from '../../ioc/types'; -import { BufferDecoder } from './decoder'; import { ProcessServiceFactory } from './processFactory'; import { PythonExecutionFactory } from './pythonExecutionFactory'; import { PythonToolExecutionService } from './pythonToolService'; -import { IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; +import { IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IBufferDecoder, BufferDecoder); serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); serviceManager.addSingleton(IPythonToolExecutionService, PythonToolExecutionService); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 13a73c2cd60f..bcab76e66b09 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -8,11 +8,6 @@ import { PythonExecInfo } from '../../pythonEnvironments/exec'; import { InterpreterInformation, PythonEnvironment } from '../../pythonEnvironments/info'; import { ExecutionInfo, IDisposable } from '../types'; -export const IBufferDecoder = Symbol('IBufferDecoder'); -export interface IBufferDecoder { - decode(buffers: Buffer[], encoding: string): string; -} - export type Output = { source: 'stdout' | 'stderr'; out: T; diff --git a/src/test/common.ts b/src/test/common.ts index 8c075671d07c..e58830b7f236 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -303,10 +303,9 @@ export function correctPathForOsType(pathToCorrect: string, os?: OSType): string * @return `SemVer` version of the Python interpreter, or `undefined` if an error occurs. */ export async function getPythonSemVer(procService?: IProcessService): Promise { - const decoder = await import('../client/common/process/decoder'); const proc = await import('../client/common/process/proc'); - const pythonProcRunner = procService ? procService : new proc.ProcessService(new decoder.BufferDecoder()); + const pythonProcRunner = procService ? procService : new proc.ProcessService(); const pyVerArgs = ['-c', 'import sys;print("{0}.{1}.{2}".format(*sys.version_info[:3]))']; return pythonProcRunner diff --git a/src/test/common/process/decoder.test.ts b/src/test/common/process/decoder.test.ts index c200227cde54..6123ce2a447c 100644 --- a/src/test/common/process/decoder.test.ts +++ b/src/test/common/process/decoder.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { encode, encodingExists } from 'iconv-lite'; -import { BufferDecoder } from '../../../client/common/process/decoder'; +import { decodeBuffer } from '../../../client/common/process/decoder'; import { initialize } from './../../initialize'; suite('Decoder', () => { @@ -13,8 +13,7 @@ suite('Decoder', () => { test('Test decoding utf8 strings', () => { const value = 'Sample input string Сделать это'; const buffer = encode(value, 'utf8'); - const decoder = new BufferDecoder(); - const decodedValue = decoder.decode([buffer]); + const decodedValue = decodeBuffer([buffer]); expect(decodedValue).equal(value, 'Decoded string is incorrect'); }); @@ -24,11 +23,10 @@ suite('Decoder', () => { } const value = 'Sample input string Сделать это'; const buffer = encode(value, 'cp866'); - const decoder = new BufferDecoder(); - let decodedValue = decoder.decode([buffer]); + let decodedValue = decodeBuffer([buffer]); expect(decodedValue).not.equal(value, 'Decoded string is the same'); - decodedValue = decoder.decode([buffer], 'cp866'); + decodedValue = decodeBuffer([buffer], 'cp866'); expect(decodedValue).equal(value, 'Decoded string is incorrect'); }); }); diff --git a/src/test/common/process/proc.exec.test.ts b/src/test/common/process/proc.exec.test.ts index 40f7e668b198..c193df95d080 100644 --- a/src/test/common/process/proc.exec.test.ts +++ b/src/test/common/process/proc.exec.test.ts @@ -6,7 +6,6 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { CancellationTokenSource } from 'vscode'; -import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessService } from '../../../client/common/process/proc'; import { StdErrError } from '../../../client/common/process/types'; import { OSType } from '../../../client/common/utils/platform'; @@ -26,7 +25,7 @@ suite('ProcessService Observable', () => { teardown(initialize); test('exec should output print statements', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const printOutput = '1234'; const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`]); @@ -42,7 +41,7 @@ suite('ProcessService Observable', () => { return this.skip(); } - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const printOutput = 'öä'; const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`]); @@ -53,7 +52,7 @@ suite('ProcessService Observable', () => { test('exec should wait for completion of program with new lines', async function () { this.timeout(5000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -79,7 +78,7 @@ suite('ProcessService Observable', () => { test('exec should wait for completion of program without new lines', async function () { this.timeout(5000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -105,7 +104,7 @@ suite('ProcessService Observable', () => { test('exec should end when cancellationToken is cancelled', async function () { this.timeout(15000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -133,7 +132,7 @@ suite('ProcessService Observable', () => { test('exec should stream stdout and stderr separately and filter output using conda related markers', async function () { this.timeout(7000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'print(">>>PYTHON-EXEC-OUTPUT")', 'import sys', @@ -176,7 +175,7 @@ suite('ProcessService Observable', () => { test('exec should merge stdout and stderr streams', async function () { this.timeout(7000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -210,7 +209,7 @@ suite('ProcessService Observable', () => { }); test('exec should throw an error with stderr output', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; const result = procService.exec(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); @@ -218,21 +217,21 @@ suite('ProcessService Observable', () => { }); test('exec should throw an error when spawn file not found', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = procService.exec(Date.now().toString(), []); await expect(result).to.eventually.be.rejected.and.to.have.property('code', 'ENOENT', 'Invalid error code'); }); test('exec should exit without no output', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = await procService.exec(pythonPath, ['-c', 'import sys', 'sys.exit()']); expect(result.stdout).equals('', 'stdout is invalid'); expect(result.stderr).equals(undefined, 'stderr is invalid'); }); test('shellExec should be able to run python and filter output using conda related markers', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const printOutput = '1234'; const result = await procService.shellExec( `"${pythonPath}" -c "print('>>>PYTHON-EXEC-OUTPUT');print('${printOutput}');print('<< { expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); }); test('shellExec should fail on invalid command', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = procService.shellExec('invalid command'); await expect(result).to.eventually.be.rejectedWith(Error, 'a', 'Expected error to be thrown'); }); test('variables can be changed after the fact', async () => { - const procService = new ProcessService(new BufferDecoder(), process.env); + const procService = new ProcessService(process.env); let result = await procService.exec(pythonPath, ['-c', `import os;print(os.environ.get("MY_TEST_VARIABLE"))`], { extraVariables: { MY_TEST_VARIABLE: 'foo' }, }); diff --git a/src/test/common/process/proc.observable.test.ts b/src/test/common/process/proc.observable.test.ts index 1df100bdc1b5..74a613f0ec1d 100644 --- a/src/test/common/process/proc.observable.test.ts +++ b/src/test/common/process/proc.observable.test.ts @@ -4,7 +4,6 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { CancellationTokenSource } from 'vscode'; -import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessService } from '../../../client/common/process/proc'; import { createDeferred } from '../../../client/common/utils/async'; import { isOs, OSType } from '../../common'; @@ -24,7 +23,7 @@ suite('ProcessService', () => { test('execObservable should stream output with new lines', function (done) { this.timeout(10000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -68,7 +67,7 @@ suite('ProcessService', () => { this.skip(); this.timeout(10000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -107,7 +106,7 @@ suite('ProcessService', () => { test('execObservable should end when cancellationToken is cancelled', function (done) { this.timeout(15000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -153,7 +152,7 @@ suite('ProcessService', () => { test('execObservable should end when process is killed', function (done) { this.timeout(15000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'import sys', 'import time', @@ -197,7 +196,7 @@ suite('ProcessService', () => { test('execObservable should stream stdout and stderr separately and removes markers related to conda run', function (done) { this.timeout(20000); - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = [ 'print(">>>PYTHON-EXEC-OUTPUT")', 'import sys', @@ -257,7 +256,7 @@ suite('ProcessService', () => { }); test('execObservable should throw an error with stderr output', (done) => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); @@ -277,7 +276,7 @@ suite('ProcessService', () => { }); test('execObservable should throw an error when spawn file not found', (done) => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = procService.execObservable(Date.now().toString(), []); expect(result).not.to.be.an('undefined', 'result is undefined.'); @@ -296,7 +295,7 @@ suite('ProcessService', () => { }); test('execObservable should exit without no output', (done) => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = procService.execObservable(pythonPath, ['-c', 'import sys', 'sys.exit()']); expect(result).not.to.be.an('undefined', 'result is undefined.'); diff --git a/src/test/common/process/processFactory.unit.test.ts b/src/test/common/process/processFactory.unit.test.ts index c9d9c1f9803b..5adcdeccecfd 100644 --- a/src/test/common/process/processFactory.unit.test.ts +++ b/src/test/common/process/processFactory.unit.test.ts @@ -5,11 +5,10 @@ import { expect } from 'chai'; import { instance, mock, verify, when } from 'ts-mockito'; import { Disposable, Uri } from 'vscode'; -import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessLogger } from '../../../client/common/process/logger'; import { ProcessService } from '../../../client/common/process/proc'; import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; -import { IBufferDecoder, IProcessLogger } from '../../../client/common/process/types'; +import { IProcessLogger } from '../../../client/common/process/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; @@ -17,13 +16,11 @@ import { IEnvironmentVariablesProvider } from '../../../client/common/variables/ suite('Process - ProcessServiceFactory', () => { let factory: ProcessServiceFactory; let envVariablesProvider: IEnvironmentVariablesProvider; - let bufferDecoder: IBufferDecoder; let processLogger: IProcessLogger; let processService: ProcessService; let disposableRegistry: IDisposableRegistry; setup(() => { - bufferDecoder = mock(BufferDecoder); envVariablesProvider = mock(EnvironmentVariablesProvider); processLogger = mock(ProcessLogger); when(processLogger.logProcess('', [], {})).thenReturn(); @@ -37,7 +34,6 @@ suite('Process - ProcessServiceFactory', () => { factory = new ProcessServiceFactory( instance(envVariablesProvider), instance(processLogger), - instance(bufferDecoder), disposableRegistry, ); }); diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts index 41413bb3632e..8035c676c188 100644 --- a/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -13,12 +13,10 @@ import { Uri } from 'vscode'; import { PythonSettings } from '../../../client/common/configSettings'; import { ConfigurationService } from '../../../client/common/configuration/service'; -import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessLogger } from '../../../client/common/process/logger'; import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; import { - IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory, @@ -75,7 +73,6 @@ suite('Process - PythonExecutionFactory', () => { suite(title(resource, interpreter), () => { let factory: PythonExecutionFactory; let activationHelper: IEnvironmentActivationService; - let bufferDecoder: IBufferDecoder; let processFactory: IProcessServiceFactory; let configService: IConfigurationService; let processLogger: IProcessLogger; @@ -89,7 +86,6 @@ suite('Process - PythonExecutionFactory', () => { setup(() => { sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); - bufferDecoder = mock(BufferDecoder); activationHelper = mock(EnvironmentActivationService); processFactory = mock(ProcessServiceFactory); configService = mock(ConfigurationService); @@ -135,7 +131,6 @@ suite('Process - PythonExecutionFactory', () => { instance(activationHelper), instance(processFactory), instance(configService), - instance(bufferDecoder), instance(pyenvs), instance(autoSelection), instance(interpreterPathExpHelper), diff --git a/src/test/common/process/serviceRegistry.unit.test.ts b/src/test/common/process/serviceRegistry.unit.test.ts index 1ee0a3ddb59f..a0187aeedffc 100644 --- a/src/test/common/process/serviceRegistry.unit.test.ts +++ b/src/test/common/process/serviceRegistry.unit.test.ts @@ -4,13 +4,11 @@ 'use strict'; import { instance, mock, verify } from 'ts-mockito'; -import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; import { PythonToolExecutionService } from '../../../client/common/process/pythonToolService'; import { registerTypes } from '../../../client/common/process/serviceRegistry'; import { - IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService, @@ -27,7 +25,6 @@ suite('Common Process Service Registry', () => { test('Ensure services are registered', async () => { registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton(IBufferDecoder, BufferDecoder)).once(); verify( serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory), ).once(); diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts index 3421cafe40be..217db6b7b47b 100644 --- a/src/test/linters/lint.functional.test.ts +++ b/src/test/linters/lint.functional.test.ts @@ -16,12 +16,10 @@ import { Product } from '../../client/common/installer/productInstaller'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { PlatformService } from '../../client/common/platform/platformService'; import { IFileSystem } from '../../client/common/platform/types'; -import { BufferDecoder } from '../../client/common/process/decoder'; import { ProcessServiceFactory } from '../../client/common/process/processFactory'; import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; import { - IBufferDecoder, IProcessLogger, IPythonExecutionFactory, IPythonToolExecutionService, @@ -694,11 +692,6 @@ class TestFixture extends BaseTestFixture { TypeMoq.MockBehavior.Strict, ); - const decoder = new BufferDecoder(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IBufferDecoder), TypeMoq.It.isAny())) - .returns(() => decoder); - const interpreterService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); serviceContainer @@ -717,7 +710,6 @@ class TestFixture extends BaseTestFixture { const procServiceFactory = new ProcessServiceFactory( envVarsService.object, processLogger.object, - decoder, disposableRegistry, ); const pyenvs: IComponentAdapter = mock(); @@ -731,7 +723,6 @@ class TestFixture extends BaseTestFixture { envActivationService.object, procServiceFactory, configService, - decoder, instance(pyenvs), instance(autoSelection), instance(interpreterPathExpHelper), diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index d97670782d96..e3c6763eace7 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -13,13 +13,11 @@ import { PlatformService } from '../client/common/platform/platformService'; import { RegistryImplementation } from '../client/common/platform/registry'; import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; import { IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; -import { BufferDecoder } from '../client/common/process/decoder'; import { ProcessService } from '../client/common/process/proc'; import { PythonExecutionFactory } from '../client/common/process/pythonExecutionFactory'; import { PythonToolExecutionService } from '../client/common/process/pythonToolService'; import { registerTypes as processRegisterTypes } from '../client/common/process/serviceRegistry'; import { - IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService, @@ -169,11 +167,10 @@ export class IocContainer { } public registerMockProcessTypes(): void { - this.serviceManager.addSingleton(IBufferDecoder, BufferDecoder); const processServiceFactory = TypeMoq.Mock.ofType(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const processService = new MockProcessService(new ProcessService(new BufferDecoder(), process.env as any)); + const processService = new MockProcessService(new ProcessService(process.env as any)); processServiceFactory.setup((f) => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); this.serviceManager.addSingletonInstance( IProcessServiceFactory, diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 9771a0b8713f..07a91f8e10de 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -12,7 +12,6 @@ import { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } f import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/constants'; import '../../../client/common/extensions'; -import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessService } from '../../../client/common/process/proc'; import { IProcessService, @@ -106,7 +105,7 @@ suite('Terminal - Code Execution Helper', () => { }); async function ensureCodeIsNormalized(source: string, expectedSource: string) { - const actualProcessService = new ProcessService(new BufferDecoder()); + const actualProcessService = new ProcessService(); processService .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((file, args, options) => From cb3c629b9fc74e082b92912266f392a72af730ed Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 14 Sep 2022 22:31:53 -0700 Subject: [PATCH 36/59] Expose interpreter quickpick API with filtering (#19839) --- .../commands/setInterpreter.ts | 50 ++++++-- src/client/interpreter/configuration/types.ts | 8 ++ src/client/interpreter/serviceRegistry.ts | 2 + .../commands/setInterpreter.unit.test.ts | 115 ++++++++++++++++++ .../interpreters/serviceRegistry.unit.test.ts | 3 +- 5 files changed, 164 insertions(+), 14 deletions(-) diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index aad89c58959b..0c04da6e3b3f 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -23,12 +23,13 @@ import { IQuickPickParameters, } from '../../../../common/utils/multiStepInput'; import { SystemVariables } from '../../../../common/variables/systemVariables'; -import { EnvironmentType } from '../../../../pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../../../pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { IInterpreterService, PythonEnvironmentsChangedEvent } from '../../../contracts'; import { isProblematicCondaEnvironment } from '../../environmentTypeComparer'; import { + IInterpreterQuickPick, IInterpreterQuickPickItem, IInterpreterSelector, IPythonPathUpdaterServiceManager, @@ -69,7 +70,7 @@ export namespace EnvGroups { } @injectable() -export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { +export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implements IInterpreterQuickPick { private readonly manualEntrySuggestion: ISpecialQuickPickItem = { label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, alwaysShow: true, @@ -126,11 +127,12 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { public async _pickInterpreter( input: IMultiStepInput, state: InterpreterStateArgs, + filter?: (i: PythonEnvironment) => boolean, ): Promise> { // If the list is refreshing, it's crucial to maintain sorting order at all // times so that the visible items do not change. const preserveOrderWhenFiltering = !!this.interpreterService.refreshPromise; - const suggestions = this._getItems(state.workspace); + const suggestions = this._getItems(state.workspace, filter); state.path = undefined; const currentInterpreterPathDisplay = this.pathUtils.getDisplayName( this.configurationService.getSettings(state.workspace).pythonPath, @@ -179,10 +181,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { // Items are in the final state as all previous callbacks have finished executing. quickPick.busy = false; // Ensure we set a recommended item after refresh has finished. - this.updateQuickPickItems(quickPick, {}, state.workspace); + this.updateQuickPickItems(quickPick, {}, state.workspace, filter); }); } - this.updateQuickPickItems(quickPick, event, state.workspace); + this.updateQuickPickItems(quickPick, event, state.workspace, filter); }, }, }); @@ -204,26 +206,33 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { return undefined; } - public _getItems(resource: Resource): QuickPickType[] { + public _getItems(resource: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined): QuickPickType[] { const suggestions: QuickPickType[] = [this.manualEntrySuggestion]; const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource); if (defaultInterpreterPathSuggestion) { suggestions.push(defaultInterpreterPathSuggestion); } - const interpreterSuggestions = this.getSuggestions(resource); + const interpreterSuggestions = this.getSuggestions(resource, filter); this.finalizeItems(interpreterSuggestions, resource); suggestions.push(...interpreterSuggestions); return suggestions; } - private getSuggestions(resource: Resource): QuickPickType[] { + private getSuggestions( + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + ): QuickPickType[] { const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - const items = this.interpreterSelector.getSuggestions(resource, !!this.interpreterService.refreshPromise); + const items = this.interpreterSelector + .getSuggestions(resource, !!this.interpreterService.refreshPromise) + .filter((i) => !filter || filter(i.interpreter)); if (this.interpreterService.refreshPromise) { // We cannot put items in groups while the list is loading as group of an item can change. return items; } - const itemsWithFullName = this.interpreterSelector.getSuggestions(resource, true); + const itemsWithFullName = this.interpreterSelector + .getSuggestions(resource, true) + .filter((i) => !filter || filter(i.interpreter)); const recommended = this.interpreterSelector.getRecommendedSuggestion( itemsWithFullName, this.workspaceService.getWorkspaceFolder(resource)?.uri, @@ -277,10 +286,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { quickPick: QuickPick, event: PythonEnvironmentsChangedEvent, resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, ) { // Active items are reset once we replace the current list with updated items, so save it. const activeItemBeforeUpdate = quickPick.activeItems.length > 0 ? quickPick.activeItems[0] : undefined; - quickPick.items = this.getUpdatedItems(quickPick.items, event, resource); + quickPick.items = this.getUpdatedItems(quickPick.items, event, resource, filter); // Ensure we maintain the same active item as before. const activeItem = activeItemBeforeUpdate ? quickPick.items.find((item) => { @@ -304,10 +314,14 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { items: readonly QuickPickType[], event: PythonEnvironmentsChangedEvent, resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, ): QuickPickType[] { const updatedItems = [...items.values()]; const areItemsGrouped = items.find((item) => isSeparatorItem(item)); const env = event.old ?? event.new; + if (filter && event.new && !filter(event.new)) { + event.new = undefined; // Remove envs we're not looking for from the list. + } let envIndex = -1; if (env) { envIndex = updatedItems.findIndex( @@ -476,7 +490,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { const wkspace = targetConfig[0].folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this._pickInterpreter(input, s), interpreterState); + await multiStep.run((input, s) => this._pickInterpreter(input, s, undefined), interpreterState); if (interpreterState.path !== undefined) { // User may choose to have an empty string stored, so variable `interpreterState.path` may be @@ -486,6 +500,16 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { } } + public async getInterpreterViaQuickPick( + workspace: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + ): Promise { + const interpreterState: InterpreterStateArgs = { path: undefined, workspace }; + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this._pickInterpreter(input, s, filter), interpreterState); + return interpreterState.path; + } + /** * Check if the interpreter that was entered exists in the list of suggestions. * If it does, it means that it had already been discovered, @@ -495,7 +519,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { */ // eslint-disable-next-line class-methods-use-this private sendInterpreterEntryTelemetry(selection: string, workspace: Resource): void { - const suggestions = this._getItems(workspace); + const suggestions = this._getItems(workspace, undefined); let interpreterPath = path.normalize(untildify(selection)); if (!path.isAbsolute(interpreterPath)) { diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 2621a297defe..1e57f4ffcfec 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -65,3 +65,11 @@ export interface IInterpreterComparer { compare(a: PythonEnvironment, b: PythonEnvironment): number; getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined; } + +export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); +export interface IInterpreterQuickPick { + getInterpreterViaQuickPick( + workspace: Resource, + filter?: (i: PythonEnvironment) => boolean, + ): Promise; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 6d36a4b9277d..cb60c370b84f 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -21,6 +21,7 @@ import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterServi import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; import { IInterpreterComparer, + IInterpreterQuickPick, IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, @@ -62,6 +63,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IExtensionSingleActivationService, SetShebangInterpreterCommand, ); + serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 1bd27b5d2cbb..b42b1aaa94e7 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -471,6 +471,121 @@ suite('Set Interpreter Command', () => { assert.deepStrictEqual(actualParameters?.items, expectedParameters.items, 'Params not equal'); }); + test('Items displayed should be filtered out if a filter is provided', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType>(); + const interpreterItems: IInterpreterQuickPickItem[] = [ + { + description: `${workspacePath}/interpreterPath1`, + detail: '', + label: 'This is the selected Python path', + path: `${workspacePath}/interpreterPath1`, + interpreter: { + id: `${workspacePath}/interpreterPath1`, + path: `${workspacePath}/interpreterPath1`, + envType: EnvironmentType.Venv, + } as PythonEnvironment, + }, + { + description: 'interpreterPath2', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath2', + interpreter: { + id: 'interpreterPath2', + path: 'interpreterPath2', + envType: EnvironmentType.VirtualEnvWrapper, + } as PythonEnvironment, + }, + { + description: 'interpreterPath3', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath3', + interpreter: { + id: 'interpreterPath3', + path: 'interpreterPath3', + envType: EnvironmentType.VirtualEnvWrapper, + } as PythonEnvironment, + }, + { + description: 'interpreterPath4', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath4', + interpreter: { + path: 'interpreterPath4', + id: 'interpreterPath4', + envType: EnvironmentType.Conda, + } as PythonEnvironment, + }, + item, + { + description: 'interpreterPath5', + detail: '', + label: 'This is the selected Python path', + path: 'interpreterPath5', + interpreter: { + path: 'interpreterPath5', + id: 'interpreterPath5', + envType: EnvironmentType.Global, + } as PythonEnvironment, + }, + ]; + interpreterSelector.reset(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => interpreterItems); + interpreterSelector + .setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => item); + const recommended = cloneDeep(item); + recommended.label = `${Octicons.Star} ${item.label}`; + recommended.description = interpreterPath; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + { label: EnvGroups.VirtualEnvWrapper, kind: QuickPickItemKind.Separator }, + interpreterItems[1], + interpreterItems[2], + { label: EnvGroups.Global, kind: QuickPickItemKind.Separator }, + interpreterItems[5], + ]; + const expectedParameters: IQuickPickParameters = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + activeItem: recommended, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter( + multiStepInput.object, + state, + (e) => e.envType === EnvironmentType.VirtualEnvWrapper || e.envType === EnvironmentType.Global, + ); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + assert.deepStrictEqual(actualParameters?.items, expectedParameters.items, 'Params not equal'); + }); + test('If system variables are used in the default interpreter path, make sure they are resolved when the path is displayed', async () => { // Create a SetInterpreterCommand instance from scratch, and use a different defaultInterpreterPath from the rest of the tests. const workspaceDefaultInterpreterPath = '${workspaceFolder}/defaultInterpreterPath'; diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 442ad7cdf3d6..a6fdce77dd3e 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -24,6 +24,7 @@ import { PythonPathUpdaterService } from '../../client/interpreter/configuration import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; import { IInterpreterComparer, + IInterpreterQuickPick, IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, @@ -53,6 +54,7 @@ suite('Interpreters - Service Registry', () => { [IExtensionSingleActivationService, InstallPythonCommand], [IExtensionSingleActivationService, InstallPythonViaTerminal], [IExtensionSingleActivationService, SetInterpreterCommand], + [IInterpreterQuickPick, SetInterpreterCommand], [IExtensionSingleActivationService, ResetInterpreterCommand], [IExtensionSingleActivationService, SetShebangInterpreterCommand], @@ -63,7 +65,6 @@ suite('Interpreters - Service Registry', () => { [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], - [IInterpreterSelector, InterpreterSelector], [IShebangCodeLensProvider, ShebangCodeLensProvider], [IInterpreterHelper, InterpreterHelper], From ad74f516cedcd2bba21d3097ad6108b032894f0b Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 15 Sep 2022 13:25:12 -0700 Subject: [PATCH 37/59] Remove DI from debug configuration providers (#19785) Closed: https://github.com/microsoft/vscode-python/issues/19766 --- .../debugConfigurationService.ts | 36 ++++- .../configuration/providers/djangoLaunch.ts | 135 ++++++++-------- .../configuration/providers/fastapiLaunch.ts | 97 ++++++------ .../configuration/providers/fileLaunch.ts | 39 +++-- .../configuration/providers/flaskLaunch.ts | 105 ++++++------- .../configuration/providers/moduleLaunch.ts | 63 ++++---- .../configuration/providers/pidAttach.ts | 37 ++--- .../providers/providerFactory.ts | 52 ------- .../configuration/providers/pyramidLaunch.ts | 147 ++++++++---------- .../configuration/providers/remoteAttach.ts | 113 +++++--------- .../debugger/extension/configuration/types.ts | 6 - .../extension/configuration/utils/common.ts | 40 +++++ .../configuration/utils/configuration.ts | 44 ++++++ .../configuration/utils/workspaceFolder.ts | 13 ++ .../debugger/extension/serviceRegistry.ts | 61 +------- src/client/debugger/extension/types.ts | 8 - .../debugConfigurationService.unit.test.ts | 13 +- .../providers/djangoLaunch.unit.test.ts | 124 +++++---------- .../providers/fastapiLaunch.unit.test.ts | 40 ++--- .../providers/fileLaunch.unit.test.ts | 8 +- .../providers/flaskLaunch.unit.test.ts | 45 ++---- .../providers/moduleLaunch.unit.test.ts | 10 +- .../providers/pidAttach.unit.test.ts | 8 +- .../providers/providerFactory.unit.test.ts | 37 ----- .../providers/pyramidLaunch.unit.test.ts | 110 +++++-------- .../providers/remoteAttach.unit.test.ts | 43 +++-- .../extension/serviceRegistry.unit.test.ts | 80 +--------- 27 files changed, 600 insertions(+), 914 deletions(-) delete mode 100644 src/client/debugger/extension/configuration/providers/providerFactory.ts create mode 100644 src/client/debugger/extension/configuration/utils/common.ts create mode 100644 src/client/debugger/extension/configuration/utils/configuration.ts create mode 100644 src/client/debugger/extension/configuration/utils/workspaceFolder.ts delete mode 100644 src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index c2e8593797f0..c5413b3662e0 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -8,14 +8,22 @@ import { cloneDeep } from 'lodash'; import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; import { DebugConfigStrings } from '../../../common/utils/localize'; import { - IMultiStepInput, IMultiStepInputFactory, InputStep, IQuickPickParameters, + MultiStepInput, } from '../../../common/utils/multiStepInput'; import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types'; -import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from './types'; +import { buildDjangoLaunchDebugConfiguration } from './providers/djangoLaunch'; +import { buildFastAPILaunchDebugConfiguration } from './providers/fastapiLaunch'; +import { buildFileLaunchDebugConfiguration } from './providers/fileLaunch'; +import { buildFlaskLaunchDebugConfiguration } from './providers/flaskLaunch'; +import { buildModuleLaunchConfiguration } from './providers/moduleLaunch'; +import { buildPidAttachConfiguration } from './providers/pidAttach'; +import { buildPyramidLaunchConfiguration } from './providers/pyramidLaunch'; +import { buildRemoteAttachConfiguration } from './providers/remoteAttach'; +import { IDebugConfigurationResolver } from './types'; @injectable() export class PythonDebugConfigurationService implements IDebugConfigurationService { @@ -27,8 +35,6 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, - @inject(IDebugConfigurationProviderFactory) - private readonly providerFactory: IDebugConfigurationProviderFactory, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, ) {} @@ -102,7 +108,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi } protected async pickDebugConfiguration( - input: IMultiStepInput, + input: MultiStepInput, state: DebugConfigurationState, ): Promise | void> { type DebugConfigurationQuickPickItem = QuickPickItem & { type: DebugConfigurationType }; @@ -148,6 +154,22 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi description: DebugConfigStrings.pyramid.selectConfiguration.description, }, ]; + const debugConfigurations = new Map< + DebugConfigurationType, + ( + input: MultiStepInput, + state: DebugConfigurationState, + ) => Promise> + >(); + debugConfigurations.set(DebugConfigurationType.launchDjango, buildDjangoLaunchDebugConfiguration); + debugConfigurations.set(DebugConfigurationType.launchFastAPI, buildFastAPILaunchDebugConfiguration); + debugConfigurations.set(DebugConfigurationType.launchFile, buildFileLaunchDebugConfiguration); + debugConfigurations.set(DebugConfigurationType.launchFlask, buildFlaskLaunchDebugConfiguration); + debugConfigurations.set(DebugConfigurationType.launchModule, buildModuleLaunchConfiguration); + debugConfigurations.set(DebugConfigurationType.pidAttach, buildPidAttachConfiguration); + debugConfigurations.set(DebugConfigurationType.remoteAttach, buildRemoteAttachConfiguration); + debugConfigurations.set(DebugConfigurationType.launchPyramid, buildPyramidLaunchConfiguration); + state.config = {}; const pick = await input.showQuickPick< DebugConfigurationQuickPickItem, @@ -159,8 +181,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi items: items, }); if (pick) { - const provider = this.providerFactory.create(pick.type); - return provider.buildConfiguration.bind(provider); + const pickedDebugConfiguration = debugConfigurations.get(pick.type)!; + return pickedDebugConfiguration(input, state); } } } diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts index 0b80f9e0cf14..da4e199fcfc4 100644 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts @@ -3,93 +3,84 @@ 'use strict'; -import { inject, injectable } from 'inversify'; +import * as vscode from 'vscode'; import * as path from 'path'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../common/application/types'; -import { IFileSystem } from '../../../../common/platform/types'; -import { IPathUtils } from '../../../../common/types'; +import * as fs from 'fs-extra'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; +import { resolveVariables } from '../utils/common'; const workspaceFolderToken = '${workspaceFolder}'; -@injectable() -export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor( - @inject(IFileSystem) private fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPathUtils) private pathUtils: IPathUtils, - ) {} - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const program = await this.getManagePyPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const defaultProgram = `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; - const config: Partial = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: program || defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - if (!program) { - const selectedProgram = await input.showInputBox({ - title: DebugConfigStrings.django.enterManagePyPath.title, - value: defaultProgram, - prompt: DebugConfigStrings.django.enterManagePyPath.prompt, - validate: (value) => this.validateManagePy(state.folder, defaultProgram, value), - }); - if (selectedProgram) { - manuallyEnteredAValue = true; - config.program = selectedProgram; - } +export async function buildDjangoLaunchDebugConfiguration( + input: MultiStepInput, + state: DebugConfigurationState, +) { + const program = await getManagePyPath(state.folder); + let manuallyEnteredAValue: boolean | undefined; + const defaultProgram = `${workspaceFolderToken}${path.sep}manage.py`; + const config: Partial = { + name: DebugConfigStrings.django.snippet.name, + type: DebuggerTypeName, + request: 'launch', + program: program || defaultProgram, + args: ['runserver'], + django: true, + justMyCode: true, + }; + if (!program) { + const selectedProgram = await input.showInputBox({ + title: DebugConfigStrings.django.enterManagePyPath.title, + value: defaultProgram, + prompt: DebugConfigStrings.django.enterManagePyPath.prompt, + validate: (value) => validateManagePy(state.folder, defaultProgram, value), + }); + if (selectedProgram) { + manuallyEnteredAValue = true; + config.program = selectedProgram; } + } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchDjango, - autoDetectedDjangoManagePyPath: !!program, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchDjango, + autoDetectedDjangoManagePyPath: !!program, + manuallyEnteredAValue, + }); + + Object.assign(state.config, config); +} + +export async function validateManagePy( + folder: vscode.WorkspaceFolder | undefined, + defaultValue: string, + selected?: string, +): Promise { + const error = DebugConfigStrings.django.enterManagePyPath.invalid; + if (!selected || selected.trim().length === 0) { + return error; } - public async validateManagePy( - folder: WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, - ): Promise { - const error = DebugConfigStrings.django.enterManagePyPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = this.resolveVariables(selected, folder ? folder.uri : undefined); - if (selected !== defaultValue && !(await this.fs.fileExists(resolvedPath))) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { - return error; - } - return; + const resolvedPath = resolveVariables(selected, undefined, folder); + + if (selected !== defaultValue && !(await fs.pathExists(resolvedPath))) { + return error; } - protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { - const systemVariables = new SystemVariables(resource, undefined, this.workspace); - return systemVariables.resolveAny(pythonPath); + if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { + return error; } + return; +} - protected async getManagePyPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); - if (await this.fs.fileExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; - } +export async function getManagePyPath(folder: vscode.WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); + if (await fs.pathExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${path.sep}manage.py`; } } diff --git a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts index a534ec21379c..6a4d3676ccab 100644 --- a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts @@ -3,69 +3,64 @@ 'use strict'; -import { inject, injectable } from 'inversify'; import * as path from 'path'; +import * as fs from 'fs-extra'; import { WorkspaceFolder } from 'vscode'; -import { IFileSystem } from '../../../../common/platform/types'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -@injectable() -export class FastAPILaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IFileSystem) private fs: IFileSystem) {} - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchFastAPI; - } - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const application = await this.getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app'], - jinja: true, - justMyCode: true, - }; +export async function buildFastAPILaunchDebugConfiguration( + input: MultiStepInput, + state: DebugConfigurationState, +) { + const application = await getApplicationPath(state.folder); + let manuallyEnteredAValue: boolean | undefined; + const config: Partial = { + name: DebugConfigStrings.fastapi.snippet.name, + type: DebuggerTypeName, + request: 'launch', + module: 'uvicorn', + args: ['main:app'], + jinja: true, + justMyCode: true, + }; - if (!application) { - const selectedPath = await input.showInputBox({ - title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, - value: 'main.py', - prompt: DebugConfigStrings.fastapi.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.fastapi.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedPath) { - manuallyEnteredAValue = true; - config.args = [`${path.basename(selectedPath, '.py').replace('/', '.')}:app`]; - } + if (!application) { + const selectedPath = await input.showInputBox({ + title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, + value: 'main.py', + prompt: DebugConfigStrings.fastapi.enterAppPathOrNamePath.prompt, + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 + ? undefined + : DebugConfigStrings.fastapi.enterAppPathOrNamePath.invalid, + ), + }); + if (selectedPath) { + manuallyEnteredAValue = true; + config.args = [`${path.basename(selectedPath, '.py').replace('/', '.')}:app`]; } + } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFastAPI, - autoDetectedFastAPIMainPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchFastAPI, + autoDetectedFastAPIMainPyPath: !!application, + manuallyEnteredAValue, + }); + Object.assign(state.config, config); +} +export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; } - protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'main.py'); - if (await this.fs.fileExists(defaultLocationOfManagePy)) { - return 'main.py'; - } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'main.py'); + if (await fs.pathExists(defaultLocationOfManagePy)) { + return 'main.py'; } } diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts index d6d51cb0528c..6fcd9d671aad 100644 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -3,31 +3,28 @@ 'use strict'; -import { injectable } from 'inversify'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { captureTelemetry } from '../../../../telemetry'; +import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -@injectable() -export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - @captureTelemetry( - EventName.DEBUGGER_CONFIGURATION_PROMPTS, - { configurationType: DebugConfigurationType.launchFile }, - false, - ) - public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { - const config: Partial = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - Object.assign(state.config, config); - } +export async function buildFileLaunchDebugConfiguration( + _input: MultiStepInput, + state: DebugConfigurationState, +) { + const config: Partial = { + name: DebugConfigStrings.file.snippet.name, + type: DebuggerTypeName, + request: 'launch', + program: '${file}', + console: 'integratedTerminal', + justMyCode: true, + }; + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchFastAPI, + }); + Object.assign(state.config, config); } diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts index 034308c73cda..4433caa6138a 100644 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -3,73 +3,68 @@ 'use strict'; -import { inject, injectable } from 'inversify'; import * as path from 'path'; +import * as fs from 'fs-extra'; import { WorkspaceFolder } from 'vscode'; -import { IFileSystem } from '../../../../common/platform/types'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -@injectable() -export class FlaskLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IFileSystem) private fs: IFileSystem) {} - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchFlask; - } - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const application = await this.getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: application || 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; +export async function buildFlaskLaunchDebugConfiguration( + input: MultiStepInput, + state: DebugConfigurationState, +) { + const application = await getApplicationPath(state.folder); + let manuallyEnteredAValue: boolean | undefined; + const config: Partial = { + name: DebugConfigStrings.flask.snippet.name, + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: application || 'app.py', + FLASK_DEBUG: '1', + }, + args: ['run', '--no-debugger', '--no-reload'], + jinja: true, + justMyCode: true, + }; - if (!application) { - const selectedApp = await input.showInputBox({ - title: DebugConfigStrings.flask.enterAppPathOrNamePath.title, - value: 'app.py', - prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedApp) { - manuallyEnteredAValue = true; - config.env!.FLASK_APP = selectedApp; - } + if (!application) { + const selectedApp = await input.showInputBox({ + title: DebugConfigStrings.flask.enterAppPathOrNamePath.title, + value: 'app.py', + prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt, + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 + ? undefined + : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid, + ), + }); + if (selectedApp) { + manuallyEnteredAValue = true; + config.env!.FLASK_APP = selectedApp; } + } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFlask, - autoDetectedFlaskAppPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchFlask, + autoDetectedFlaskAppPyPath: !!application, + manuallyEnteredAValue, + }); + Object.assign(state.config, config); +} +export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; } - protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); - if (await this.fs.fileExists(defaultLocationOfManagePy)) { - return 'app.py'; - } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); + if (await fs.pathExists(defaultLocationOfManagePy)) { + return 'app.py'; } } diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts index 9134655e1143..1b644833f593 100644 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts @@ -3,44 +3,43 @@ 'use strict'; -import { injectable } from 'inversify'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -@injectable() -export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - const selectedModule = await input.showInputBox({ - title: DebugConfigStrings.module.enterModule.title, - value: config.module || DebugConfigStrings.module.enterModule.default, - prompt: DebugConfigStrings.module.enterModule.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.module.enterModule.invalid, - ), - }); - if (selectedModule) { - manuallyEnteredAValue = true; - config.module = selectedModule; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchModule, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); +export async function buildModuleLaunchConfiguration( + input: MultiStepInput, + state: DebugConfigurationState, +) { + let manuallyEnteredAValue: boolean | undefined; + const config: Partial = { + name: DebugConfigStrings.module.snippet.name, + type: DebuggerTypeName, + request: 'launch', + module: DebugConfigStrings.module.snippet.default, + justMyCode: true, + }; + const selectedModule = await input.showInputBox({ + title: DebugConfigStrings.module.enterModule.title, + value: config.module || DebugConfigStrings.module.enterModule.default, + prompt: DebugConfigStrings.module.enterModule.prompt, + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 ? undefined : DebugConfigStrings.module.enterModule.invalid, + ), + }); + if (selectedModule) { + manuallyEnteredAValue = true; + config.module = selectedModule; } + + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchModule, + manuallyEnteredAValue, + }); + Object.assign(state.config, config); } diff --git a/src/client/debugger/extension/configuration/providers/pidAttach.ts b/src/client/debugger/extension/configuration/providers/pidAttach.ts index 8225ec175e65..c9bb7656d6ca 100644 --- a/src/client/debugger/extension/configuration/providers/pidAttach.ts +++ b/src/client/debugger/extension/configuration/providers/pidAttach.ts @@ -3,30 +3,27 @@ 'use strict'; -import { injectable } from 'inversify'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { captureTelemetry } from '../../../../telemetry'; +import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -@injectable() -export class PidAttachDebugConfigurationProvider implements IDebugConfigurationProvider { - @captureTelemetry( - EventName.DEBUGGER_CONFIGURATION_PROMPTS, - { configurationType: DebugConfigurationType.pidAttach }, - false, - ) - public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { - const config: Partial = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - Object.assign(state.config, config); - } +export async function buildPidAttachConfiguration( + _input: MultiStepInput, + state: DebugConfigurationState, +) { + const config: Partial = { + name: DebugConfigStrings.attachPid.snippet.name, + type: DebuggerTypeName, + request: 'attach', + processId: '${command:pickProcess}', + justMyCode: true, + }; + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.pidAttach, + }); + Object.assign(state.config, config); } diff --git a/src/client/debugger/extension/configuration/providers/providerFactory.ts b/src/client/debugger/extension/configuration/providers/providerFactory.ts deleted file mode 100644 index 18f5a18a9ef4..000000000000 --- a/src/client/debugger/extension/configuration/providers/providerFactory.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; -import { IDebugConfigurationProviderFactory } from '../types'; - -@injectable() -export class DebugConfigurationProviderFactory implements IDebugConfigurationProviderFactory { - private readonly providers: Map; - constructor( - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.launchFastAPI) - fastapiProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.launchFlask) - flaskProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.launchDjango) - djangoProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.launchModule) - moduleProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.launchFile) - fileProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.launchPyramid) - pyramidProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.remoteAttach) - remoteAttachProvider: IDebugConfigurationProvider, - @inject(IDebugConfigurationProvider) - @named(DebugConfigurationType.pidAttach) - pidAttachProvider: IDebugConfigurationProvider, - ) { - this.providers = new Map(); - this.providers.set(DebugConfigurationType.launchDjango, djangoProvider); - this.providers.set(DebugConfigurationType.launchFastAPI, fastapiProvider); - this.providers.set(DebugConfigurationType.launchFlask, flaskProvider); - this.providers.set(DebugConfigurationType.launchFile, fileProvider); - this.providers.set(DebugConfigurationType.launchModule, moduleProvider); - this.providers.set(DebugConfigurationType.launchPyramid, pyramidProvider); - this.providers.set(DebugConfigurationType.remoteAttach, remoteAttachProvider); - this.providers.set(DebugConfigurationType.pidAttach, pidAttachProvider); - } - public create(configurationType: DebugConfigurationType): IDebugConfigurationProvider { - return this.providers.get(configurationType)!; - } -} diff --git a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts index 04233df11039..dd82518720c4 100644 --- a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts @@ -3,105 +3,94 @@ 'use strict'; -import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../common/application/types'; -import { IFileSystem } from '../../../../common/platform/types'; -import { IPathUtils } from '../../../../common/types'; +import * as fs from 'fs-extra'; +import { WorkspaceFolder } from 'vscode'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; import * as nls from 'vscode-nls'; +import { resolveVariables } from '../utils/common'; const localize: nls.LocalizeFunc = nls.loadMessageBundle(); const workspaceFolderToken = '${workspaceFolder}'; -@injectable() -export class PyramidLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor( - @inject(IFileSystem) private fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPathUtils) private pathUtils: IPathUtils, - ) {} - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const iniPath = await this.getDevelopmentIniPath(state.folder); - const defaultIni = `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; - let manuallyEnteredAValue: boolean | undefined; +export async function buildPyramidLaunchConfiguration( + input: MultiStepInput, + state: DebugConfigurationState, +) { + const iniPath = await getDevelopmentIniPath(state.folder); + const defaultIni = `${workspaceFolderToken}${path.sep}development.ini`; + let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [iniPath || defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; + const config: Partial = { + name: DebugConfigStrings.pyramid.snippet.name, + type: DebuggerTypeName, + request: 'launch', + module: 'pyramid.scripts.pserve', + args: [iniPath || defaultIni], + pyramid: true, + jinja: true, + justMyCode: true, + }; - if (!iniPath) { - const selectedIniPath = await input.showInputBox({ - title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title, - value: defaultIni, - prompt: localize( - 'debug.pyramidEnterDevelopmentIniPathPrompt', - 'Enter the path to development.ini ({0} points to the root of the current workspace folder)', - workspaceFolderToken, - ), - validate: (value) => this.validateIniPath(state ? state.folder : undefined, defaultIni, value), - }); - if (selectedIniPath) { - manuallyEnteredAValue = true; - config.args = [selectedIniPath]; - } + if (!iniPath) { + const selectedIniPath = await input.showInputBox({ + title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title, + value: defaultIni, + prompt: localize( + 'debug.pyramidEnterDevelopmentIniPathPrompt', + 'Enter the path to development.ini ({0} points to the root of the current workspace folder)', + workspaceFolderToken, + ), + validate: (value) => validateIniPath(state ? state.folder : undefined, defaultIni, value), + }); + if (selectedIniPath) { + manuallyEnteredAValue = true; + config.args = [selectedIniPath]; } + } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchPyramid, - autoDetectedPyramidIniPath: !!iniPath, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchPyramid, + autoDetectedPyramidIniPath: !!iniPath, + manuallyEnteredAValue, + }); + Object.assign(state.config, config); +} + +export async function validateIniPath( + folder: WorkspaceFolder | undefined, + defaultValue: string, + selected?: string, +): Promise { + if (!folder) { + return; } - public async validateIniPath( - folder: WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, - ): Promise { - if (!folder) { - return; - } - const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = this.resolveVariables(selected, folder.uri); - if (selected !== defaultValue && !(await this.fs.fileExists(resolvedPath))) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { - return error; - } + const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid; + if (!selected || selected.trim().length === 0) { + return error; } - protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { - const systemVariables = new SystemVariables(resource, undefined, this.workspace); - return systemVariables.resolveAny(pythonPath); + const resolvedPath = resolveVariables(selected, undefined, folder); + if (selected !== defaultValue && !fs.pathExists(resolvedPath)) { + return error; } + if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { + return error; + } +} - protected async getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); - if (await this.fs.fileExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; - } +export async function getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); + if (await fs.pathExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${path.sep}development.ini`; } } diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts index c97ade2a83ef..a43c48b664af 100644 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ b/src/client/debugger/extension/configuration/providers/remoteAttach.ts @@ -3,90 +3,59 @@ 'use strict'; -import { injectable } from 'inversify'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; +import { configurePort } from '../utils/configuration'; const defaultHost = 'localhost'; const defaultPort = 5678; -@injectable() -export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurationProvider { - public async buildConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ): Promise | void> { - const config: Partial = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: defaultHost, - port: defaultPort, +export async function buildRemoteAttachConfiguration( + input: MultiStepInput, + state: DebugConfigurationState, +): Promise | void> { + const config: Partial = { + name: DebugConfigStrings.attach.snippet.name, + type: DebuggerTypeName, + request: 'attach', + connect: { + host: defaultHost, + port: defaultPort, + }, + pathMappings: [ + { + localRoot: '${workspaceFolder}', + remoteRoot: '.', }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; + ], + justMyCode: true, + }; - const connect = config.connect!; - connect.host = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemoteHost.title, - step: 1, - totalSteps: 2, - value: connect.host || defaultHost, - prompt: DebugConfigStrings.attach.enterRemoteHost.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid, - ), - }); - if (!connect.host) { - connect.host = defaultHost; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.host !== defaultHost, - }); - Object.assign(state.config, config); - return (_) => this.configurePort(input, state.config); + const connect = config.connect!; + connect.host = await input.showInputBox({ + title: DebugConfigStrings.attach.enterRemoteHost.title, + step: 1, + totalSteps: 2, + value: connect.host || defaultHost, + prompt: DebugConfigStrings.attach.enterRemoteHost.prompt, + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid, + ), + }); + if (!connect.host) { + connect.host = defaultHost; } - protected async configurePort( - input: MultiStepInput, - config: Partial, - ) { - const connect = config.connect || (config.connect = {}); - const port = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemotePort.title, - step: 2, - totalSteps: 2, - value: (connect.port || defaultPort).toString(), - prompt: DebugConfigStrings.attach.enterRemotePort.prompt, - validate: (value) => - Promise.resolve( - value && /^\d+$/.test(value.trim()) ? undefined : DebugConfigStrings.attach.enterRemotePort.invalid, - ), - }); - if (port && /^\d+$/.test(port.trim())) { - connect.port = parseInt(port, 10); - } - if (!connect.port) { - connect.port = defaultPort; - } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.port !== defaultPort, - }); - } + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.remoteAttach, + manuallyEnteredAValue: connect.host !== defaultHost, + }); + Object.assign(state.config, config); + return (_) => configurePort(input, state.config); } diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts index 0dbf32c8d68b..c888fc89ddec 100644 --- a/src/client/debugger/extension/configuration/types.ts +++ b/src/client/debugger/extension/configuration/types.ts @@ -4,7 +4,6 @@ 'use strict'; import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../types'; export const IDebugConfigurationResolver = Symbol('IDebugConfigurationResolver'); export interface IDebugConfigurationResolver { @@ -21,11 +20,6 @@ export interface IDebugConfigurationResolver { ): Promise; } -export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); -export interface IDebugConfigurationProviderFactory { - create(configurationType: DebugConfigurationType): IDebugConfigurationProvider; -} - export const ILaunchJsonReader = Symbol('ILaunchJsonReader'); export interface ILaunchJsonReader { getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise; diff --git a/src/client/debugger/extension/configuration/utils/common.ts b/src/client/debugger/extension/configuration/utils/common.ts new file mode 100644 index 000000000000..4767d7c928e0 --- /dev/null +++ b/src/client/debugger/extension/configuration/utils/common.ts @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { WorkspaceFolder } from 'vscode'; +import { getWorkspaceFolder } from './workspaceFolder'; + +/** + * @returns whether the provided parameter is a JavaScript String or not. + */ +function isString(str: any): str is string { + if (typeof str === 'string' || str instanceof String) { + return true; + } + + return false; +} + +export function resolveVariables( + value: string, + rootFolder: string | undefined, + folder: WorkspaceFolder | undefined, +): string { + const workspace = folder ? getWorkspaceFolder(folder.uri) : undefined; + const variablesObject: { [key: string]: any } = {}; + variablesObject.workspaceFolder = workspace ? workspace.uri.fsPath : rootFolder; + + const regexp = /\$\{(.*?)\}/g; + return value.replace(regexp, (match: string, name: string) => { + const newValue = variablesObject[name]; + if (isString(newValue)) { + return newValue; + } + return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match; + }); +} diff --git a/src/client/debugger/extension/configuration/utils/configuration.ts b/src/client/debugger/extension/configuration/utils/configuration.ts new file mode 100644 index 000000000000..37fb500dbfdd --- /dev/null +++ b/src/client/debugger/extension/configuration/utils/configuration.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { AttachRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType } from '../../types'; + +const defaultPort = 5678; + +export async function configurePort( + input: MultiStepInput, + config: Partial, +): Promise { + const connect = config.connect || (config.connect = {}); + const port = await input.showInputBox({ + title: DebugConfigStrings.attach.enterRemotePort.title, + step: 2, + totalSteps: 2, + value: (connect.port || defaultPort).toString(), + prompt: DebugConfigStrings.attach.enterRemotePort.prompt, + validate: (value) => + Promise.resolve( + value && /^\d+$/.test(value.trim()) ? undefined : DebugConfigStrings.attach.enterRemotePort.invalid, + ), + }); + if (port && /^\d+$/.test(port.trim())) { + connect.port = parseInt(port, 10); + } + if (!connect.port) { + connect.port = defaultPort; + } + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.remoteAttach, + manuallyEnteredAValue: connect.port !== defaultPort, + }); +} diff --git a/src/client/debugger/extension/configuration/utils/workspaceFolder.ts b/src/client/debugger/extension/configuration/utils/workspaceFolder.ts new file mode 100644 index 000000000000..ddd98c751562 --- /dev/null +++ b/src/client/debugger/extension/configuration/utils/workspaceFolder.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; + +export function getWorkspaceFolder(uri: vscode.Uri): vscode.WorkspaceFolder | undefined { + return vscode.workspace.getWorkspaceFolder(uri); +} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 3ffdf5193d2b..4322e9d31df5 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -19,31 +19,16 @@ import { LaunchJsonCompletionProvider } from './configuration/launch.json/comple import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand'; import { LaunchJsonReader } from './configuration/launch.json/launchJsonReader'; import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; -import { DjangoLaunchDebugConfigurationProvider } from './configuration/providers/djangoLaunch'; -import { FastAPILaunchDebugConfigurationProvider } from './configuration/providers/fastapiLaunch'; -import { FileLaunchDebugConfigurationProvider } from './configuration/providers/fileLaunch'; -import { FlaskLaunchDebugConfigurationProvider } from './configuration/providers/flaskLaunch'; -import { ModuleLaunchDebugConfigurationProvider } from './configuration/providers/moduleLaunch'; -import { PidAttachDebugConfigurationProvider } from './configuration/providers/pidAttach'; -import { DebugConfigurationProviderFactory } from './configuration/providers/providerFactory'; -import { PyramidLaunchDebugConfigurationProvider } from './configuration/providers/pyramidLaunch'; -import { RemoteAttachDebugConfigurationProvider } from './configuration/providers/remoteAttach'; import { AttachConfigurationResolver } from './configuration/resolvers/attach'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; -import { - IDebugConfigurationProviderFactory, - IDebugConfigurationResolver, - ILaunchJsonReader, -} from './configuration/types'; +import { IDebugConfigurationResolver, ILaunchJsonReader } from './configuration/types'; import { DebugCommands } from './debugCommands'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; import { - DebugConfigurationType, IDebugAdapterDescriptorFactory, - IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner, IDebugSessionLoggingFactory, @@ -85,50 +70,6 @@ export function registerTypes(serviceManager: IServiceManager) { AttachConfigurationResolver, 'attach', ); - serviceManager.addSingleton( - IDebugConfigurationProviderFactory, - DebugConfigurationProviderFactory, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - FileLaunchDebugConfigurationProvider, - DebugConfigurationType.launchFile, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - DjangoLaunchDebugConfigurationProvider, - DebugConfigurationType.launchDjango, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - FastAPILaunchDebugConfigurationProvider, - DebugConfigurationType.launchFastAPI, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - FlaskLaunchDebugConfigurationProvider, - DebugConfigurationType.launchFlask, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - RemoteAttachDebugConfigurationProvider, - DebugConfigurationType.remoteAttach, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - ModuleLaunchDebugConfigurationProvider, - DebugConfigurationType.launchModule, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - PyramidLaunchDebugConfigurationProvider, - DebugConfigurationType.launchPyramid, - ); - serviceManager.addSingleton( - IDebugConfigurationProvider, - PidAttachDebugConfigurationProvider, - DebugConfigurationType.pidAttach, - ); serviceManager.addSingleton( IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper, diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 273a398f6e40..1e5b724975a9 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -13,7 +13,6 @@ import { WorkspaceFolder, } from 'vscode'; -import { InputStep, MultiStepInput } from '../../common/utils/multiStepInput'; import { DebugConfigurationArguments } from '../types'; export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); @@ -27,18 +26,11 @@ export interface IDebuggerBanner { initialize(): void; } -export const IDebugConfigurationProvider = Symbol('IDebugConfigurationProvider'); export type DebugConfigurationState = { config: Partial; folder?: WorkspaceFolder; token?: CancellationToken; }; -export interface IDebugConfigurationProvider { - buildConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ): Promise | void>; -} export enum DebugConfigurationType { launchFile = 'launchFile', diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 8495d4820c0a..85c45407a137 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -4,12 +4,10 @@ 'use strict'; import { expect } from 'chai'; -import { instance, mock } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; -import { IMultiStepInput, IMultiStepInputFactory } from '../../../../client/common/utils/multiStepInput'; +import { IMultiStepInputFactory, MultiStepInput } from '../../../../client/common/utils/multiStepInput'; import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { DebugConfigurationProviderFactory } from '../../../../client/debugger/extension/configuration/providers/providerFactory'; import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; import { DebugConfigurationState } from '../../../../client/debugger/extension/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; @@ -19,11 +17,10 @@ suite('Debugging - Configuration Service', () => { let launchResolver: typemoq.IMock>; let configService: TestPythonDebugConfigurationService; let multiStepFactory: typemoq.IMock; - let providerFactory: DebugConfigurationProviderFactory; class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { public async pickDebugConfiguration( - input: IMultiStepInput, + input: MultiStepInput, state: DebugConfigurationState, ) { return super.pickDebugConfiguration(input, state); @@ -33,12 +30,10 @@ suite('Debugging - Configuration Service', () => { attachResolver = typemoq.Mock.ofType>(); launchResolver = typemoq.Mock.ofType>(); multiStepFactory = typemoq.Mock.ofType(); - providerFactory = mock(DebugConfigurationProviderFactory); configService = new TestPythonDebugConfigurationService( attachResolver.object, launchResolver.object, - instance(providerFactory), multiStepFactory.object, ); }); @@ -93,7 +88,7 @@ suite('Debugging - Configuration Service', () => { }); test('Picker should be displayed', async () => { const state = ({ configs: [], folder: {}, token: undefined } as any) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); + const multiStepInput = typemoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(typemoq.It.isAny())) .returns(() => Promise.resolve(undefined as any)) @@ -105,7 +100,7 @@ suite('Debugging - Configuration Service', () => { }); test('Existing Configuration items must be removed before displaying picker', async () => { const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as any) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); + const multiStepInput = typemoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(typemoq.It.isAny())) .returns(() => Promise.resolve(undefined as any)) diff --git a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts index 6423e19ad76b..479fdfebb53e 100644 --- a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts @@ -3,153 +3,104 @@ 'use strict'; +import { Uri } from 'vscode'; import { expect } from 'chai'; import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../../client/common/application/workspace'; -import { FileSystem } from '../../../../../client/common/platform/fileSystem'; -import { PathUtils } from '../../../../../client/common/platform/pathUtils'; -import { IFileSystem } from '../../../../../client/common/platform/types'; -import { IPathUtils } from '../../../../../client/common/types'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DjangoLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; +import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; +import * as workspaceFolder from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; +import * as djangoLaunch from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; suite('Debugging - Configuration Provider Django', () => { - let fs: IFileSystem; - let workspaceService: IWorkspaceService; - let pathUtils: IPathUtils; - let provider: TestDjangoLaunchDebugConfigurationProvider; + let pathExistsStub: sinon.SinonStub; + let pathSeparatorStub: sinon.SinonStub; + let workspaceStub: sinon.SinonStub; let input: MultiStepInput; - class TestDjangoLaunchDebugConfigurationProvider extends DjangoLaunchDebugConfigurationProvider { - public resolveVariables(pythonPath: string, resource: Uri | undefined): string { - return super.resolveVariables(pythonPath, resource); - } - - public async getManagePyPath(folder: WorkspaceFolder): Promise { - return super.getManagePyPath(folder); - } - } + setup(() => { - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - pathUtils = mock(PathUtils); input = mock>(MultiStepInput); - provider = new TestDjangoLaunchDebugConfigurationProvider( - instance(fs), - instance(workspaceService), - instance(pathUtils), - ); + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathSeparatorStub = sinon.stub(path, 'sep'); + workspaceStub = sinon.stub(workspaceFolder, 'getWorkspaceFolder'); + }); + teardown(() => { + sinon.restore(); }); test("getManagePyPath should return undefined if file doesn't exist", async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - when(fs.fileExists(managePyPath)).thenResolve(false); - - const file = await provider.getManagePyPath(folder); + pathExistsStub.withArgs(managePyPath).resolves(false); + const file = await djangoLaunch.getManagePyPath(folder); expect(file).to.be.equal(undefined, 'Should return undefined'); }); test('getManagePyPath should file path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - - when(pathUtils.separator).thenReturn('-'); - when(fs.fileExists(managePyPath)).thenResolve(true); - - const file = await provider.getManagePyPath(folder); + pathExistsStub.withArgs(managePyPath).resolves(true); + pathSeparatorStub.value('-'); + const file = await djangoLaunch.getManagePyPath(folder); expect(file).to.be.equal('${workspaceFolder}-manage.py'); }); test('Resolve variables (with resource)', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); - - const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); + workspaceStub.returns(folder); + const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); }); test('Validation of path should return errors if path is undefined', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateManagePy(folder, ''); + const error = await djangoLaunch.validateManagePy(folder, ''); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if path is empty', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateManagePy(folder, '', ''); + const error = await djangoLaunch.validateManagePy(folder, '', ''); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if resolved path is empty', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => ''; - - const error = await provider.validateManagePy(folder, '', 'x'); + const error = await djangoLaunch.validateManagePy(folder, '', 'x'); expect(error).to.be.length.greaterThan(1); }); test("Validation of path should return errors if resolved path doesn't exist", async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz'; - - when(fs.fileExists('xyz')).thenResolve(false); - const error = await provider.validateManagePy(folder, '', 'x'); + pathExistsStub.withArgs('xyz').resolves(false); + const error = await djangoLaunch.validateManagePy(folder, '', 'x'); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if resolved path is non-python', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.txt'; - - when(fs.fileExists('xyz.txt')).thenResolve(true); - const error = await provider.validateManagePy(folder, '', 'x'); + pathExistsStub.withArgs('xyz.txt').resolves(true); + const error = await djangoLaunch.validateManagePy(folder, '', 'x'); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if resolved path is python', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.py'; - - when(fs.fileExists('xyz.py')).thenResolve(true); - const error = await provider.validateManagePy(folder, '', 'x'); + pathExistsStub.withArgs('xyz.py').resolves(true); + const error = await djangoLaunch.validateManagePy(folder, '', 'xyz.py'); expect(error).to.be.equal(undefined, 'should not have errors'); }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getManagePyPath = () => Promise.resolve('xyz.py'); - when(pathUtils.separator).thenReturn('-'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: 'xyz.py', - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); test('Launch JSON with selected managepy path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getManagePyPath = () => Promise.resolve(undefined); - when(pathUtils.separator).thenReturn('-'); + pathSeparatorStub.value('-'); when(input.showInputBox(anything())).thenResolve('hello'); - - await provider.buildConfiguration(instance(input), state); + await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.django.snippet.name, @@ -166,14 +117,11 @@ suite('Debugging - Configuration Provider Django', () => { test('Launch JSON with default managepy path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getManagePyPath = () => Promise.resolve(undefined); const workspaceFolderToken = '${workspaceFolder}'; const defaultProgram = `${workspaceFolderToken}-manage.py`; - - when(pathUtils.separator).thenReturn('-'); + pathSeparatorStub.value('-'); when(input.showInputBox(anything())).thenResolve(); - - await provider.buildConfiguration(instance(input), state); + await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.django.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts index 35ef98ce9cf8..f6c20985e4da 100644 --- a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts @@ -5,55 +5,48 @@ import { expect } from 'chai'; import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { FileSystem } from '../../../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../../../client/common/platform/types'; +import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { FastAPILaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/fastapiLaunch'; +import * as fastApiLaunch from '../../../../../client/debugger/extension/configuration/providers/fastapiLaunch'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; suite('Debugging - Configuration Provider FastAPI', () => { - let fs: IFileSystem; - let provider: TestFastAPILaunchDebugConfigurationProvider; let input: MultiStepInput; - class TestFastAPILaunchDebugConfigurationProvider extends FastAPILaunchDebugConfigurationProvider { - public async getApplicationPath(folder: WorkspaceFolder): Promise { - return super.getApplicationPath(folder); - } - } + let pathExistsStub: sinon.SinonStub; + setup(() => { - fs = mock(FileSystem); input = mock>(MultiStepInput); - provider = new TestFastAPILaunchDebugConfigurationProvider(instance(fs)); + pathExistsStub = sinon.stub(fs, 'pathExists'); + }); + teardown(() => { + sinon.restore(); }); test("getApplicationPath should return undefined if file doesn't exist", async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - when(fs.fileExists(appPyPath)).thenResolve(false); - - const file = await provider.getApplicationPath(folder); + pathExistsStub.withArgs(appPyPath).resolves(false); + const file = await fastApiLaunch.getApplicationPath(folder); expect(file).to.be.equal(undefined, 'Should return undefined'); }); test('getApplicationPath should find path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - - when(fs.fileExists(appPyPath)).thenResolve(true); - - const file = await provider.getApplicationPath(folder); + pathExistsStub.withArgs(appPyPath).resolves(true); + const file = await fastApiLaunch.getApplicationPath(folder); expect(file).to.be.equal('main.py'); }); test('Launch JSON with valid python path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve('xyz.py'); - await provider.buildConfiguration(instance(input), state); + await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.fastapi.snippet.name, @@ -70,11 +63,10 @@ suite('Debugging - Configuration Provider FastAPI', () => { test('Launch JSON with selected app path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve(undefined); when(input.showInputBox(anything())).thenResolve('main'); - await provider.buildConfiguration(instance(input), state); + await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.fastapi.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts index c074ad33a01c..60f2b199bbd2 100644 --- a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts @@ -8,18 +8,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { FileLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; +import { buildFileLaunchDebugConfiguration } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; suite('Debugging - Configuration Provider File', () => { - let provider: FileLaunchDebugConfigurationProvider; - setup(() => { - provider = new FileLaunchDebugConfigurationProvider(); - }); test('Launch JSON with default managepy path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - await provider.buildConfiguration(undefined as any, state); + await buildFileLaunchDebugConfiguration(undefined as any, state); const config = { name: DebugConfigStrings.file.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts index c8beab640ab1..08fb5259b282 100644 --- a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts @@ -5,55 +5,47 @@ import { expect } from 'chai'; import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { FileSystem } from '../../../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../../../client/common/platform/types'; +import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { FlaskLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; +import * as flaskLaunch from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; suite('Debugging - Configuration Provider Flask', () => { - let fs: IFileSystem; - let provider: TestFlaskLaunchDebugConfigurationProvider; + let pathExistsStub: sinon.SinonStub; let input: MultiStepInput; - class TestFlaskLaunchDebugConfigurationProvider extends FlaskLaunchDebugConfigurationProvider { - public async getApplicationPath(folder: WorkspaceFolder): Promise { - return super.getApplicationPath(folder); - } - } setup(() => { - fs = mock(FileSystem); input = mock>(MultiStepInput); - provider = new TestFlaskLaunchDebugConfigurationProvider(instance(fs)); + pathExistsStub = sinon.stub(fs, 'pathExists'); + }); + teardown(() => { + sinon.restore(); }); test("getApplicationPath should return undefined if file doesn't exist", async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - when(fs.fileExists(appPyPath)).thenResolve(false); - - const file = await provider.getApplicationPath(folder); + pathExistsStub.withArgs(appPyPath).resolves(false); + const file = await flaskLaunch.getApplicationPath(folder); expect(file).to.be.equal(undefined, 'Should return undefined'); }); test('getApplicationPath should file path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - - when(fs.fileExists(appPyPath)).thenResolve(true); - - const file = await provider.getApplicationPath(folder); + pathExistsStub.withArgs(appPyPath).resolves(true); + const file = await flaskLaunch.getApplicationPath(folder); expect(file).to.be.equal('app.py'); }); test('Launch JSON with valid python path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve('xyz.py'); - await provider.buildConfiguration(instance(input), state); + await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.flask.snippet.name, @@ -61,7 +53,7 @@ suite('Debugging - Configuration Provider Flask', () => { request: 'launch', module: 'flask', env: { - FLASK_APP: 'xyz.py', + FLASK_APP: 'app.py', FLASK_DEBUG: '1', }, args: ['run', '--no-debugger', '--no-reload'], @@ -74,11 +66,10 @@ suite('Debugging - Configuration Provider Flask', () => { test('Launch JSON with selected app path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve(undefined); when(input.showInputBox(anything())).thenResolve('hello'); - await provider.buildConfiguration(instance(input), state); + await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.flask.snippet.name, @@ -99,11 +90,9 @@ suite('Debugging - Configuration Provider Flask', () => { test('Launch JSON with default managepy path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve(undefined); - when(input.showInputBox(anything())).thenResolve(); - await provider.buildConfiguration(instance(input), state); + await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); const config = { name: DebugConfigStrings.flask.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts index c0571a1bf30f..2508db506ca2 100644 --- a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts @@ -10,14 +10,10 @@ import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { ModuleLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; +import { buildModuleLaunchConfiguration } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; suite('Debugging - Configuration Provider Module', () => { - let provider: ModuleLaunchDebugConfigurationProvider; - setup(() => { - provider = new ModuleLaunchDebugConfigurationProvider(); - }); test('Launch JSON with default module name', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; @@ -25,7 +21,7 @@ suite('Debugging - Configuration Provider Module', () => { when(input.showInputBox(anything())).thenResolve(); - await provider.buildConfiguration(instance(input), state); + await buildModuleLaunchConfiguration(instance(input), state); const config = { name: DebugConfigStrings.module.snippet.name, @@ -44,7 +40,7 @@ suite('Debugging - Configuration Provider Module', () => { when(input.showInputBox(anything())).thenResolve('hello'); - await provider.buildConfiguration(instance(input), state); + await buildModuleLaunchConfiguration(instance(input), state); const config = { name: DebugConfigStrings.module.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts index 696179ea9a12..db9de7e0f58c 100644 --- a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts @@ -8,18 +8,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { PidAttachDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; +import { buildPidAttachConfiguration } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; suite('Debugging - Configuration Provider File', () => { - let provider: PidAttachDebugConfigurationProvider; - setup(() => { - provider = new PidAttachDebugConfigurationProvider(); - }); test('Launch JSON with default process id', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - await provider.buildConfiguration(undefined as any, state); + await buildPidAttachConfiguration(undefined as any, state); const config = { name: DebugConfigStrings.attachPid.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts b/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts deleted file mode 100644 index a786347ed8d1..000000000000 --- a/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { getNamesAndValues } from '../../../../../client/common/utils/enum'; -import { DebugConfigurationProviderFactory } from '../../../../../client/debugger/extension/configuration/providers/providerFactory'; -import { IDebugConfigurationProviderFactory } from '../../../../../client/debugger/extension/configuration/types'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Factory', () => { - let mappedProviders: Map; - let factory: IDebugConfigurationProviderFactory; - setup(() => { - mappedProviders = new Map(); - getNamesAndValues(DebugConfigurationType).forEach((item) => { - mappedProviders.set(item.value, (item.value as any) as IDebugConfigurationProvider); - }); - factory = new DebugConfigurationProviderFactory( - mappedProviders.get(DebugConfigurationType.launchFastAPI)!, - mappedProviders.get(DebugConfigurationType.launchFlask)!, - mappedProviders.get(DebugConfigurationType.launchDjango)!, - mappedProviders.get(DebugConfigurationType.launchModule)!, - mappedProviders.get(DebugConfigurationType.launchFile)!, - mappedProviders.get(DebugConfigurationType.launchPyramid)!, - mappedProviders.get(DebugConfigurationType.remoteAttach)!, - mappedProviders.get(DebugConfigurationType.pidAttach)!, - ); - }); - getNamesAndValues(DebugConfigurationType).forEach((item) => { - test(`Configuration Provider for ${item.name}`, () => { - const provider = factory.create(item.value); - expect(provider).to.equal(mappedProviders.get(item.value)); - }); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts index 04a74dfd819b..8a5d29206180 100644 --- a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts @@ -5,137 +5,109 @@ import { expect } from 'chai'; import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../../client/common/application/workspace'; -import { FileSystem } from '../../../../../client/common/platform/fileSystem'; -import { PathUtils } from '../../../../../client/common/platform/pathUtils'; -import { IFileSystem } from '../../../../../client/common/platform/types'; -import { IPathUtils } from '../../../../../client/common/types'; +import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { PyramidLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; +import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; +import * as workspaceFolder from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; +import * as pyramidLaunch from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; suite('Debugging - Configuration Provider Pyramid', () => { - let fs: IFileSystem; - let workspaceService: IWorkspaceService; - let pathUtils: IPathUtils; - let provider: TestPyramidLaunchDebugConfigurationProvider; let input: MultiStepInput; - class TestPyramidLaunchDebugConfigurationProvider extends PyramidLaunchDebugConfigurationProvider { - public resolveVariables(pythonPath: string, resource: Uri | undefined): string { - return super.resolveVariables(pythonPath, resource); - } - - public async getDevelopmentIniPath(folder: WorkspaceFolder): Promise { - return super.getDevelopmentIniPath(folder); - } - } + let pathExistsStub: sinon.SinonStub; + let pathSeparatorStub: sinon.SinonStub; + let workspaceStub: sinon.SinonStub; + setup(() => { - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - pathUtils = mock(PathUtils); input = mock>(MultiStepInput); - provider = new TestPyramidLaunchDebugConfigurationProvider( - instance(fs), - instance(workspaceService), - instance(pathUtils), - ); + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathSeparatorStub = sinon.stub(path, 'sep'); + workspaceStub = sinon.stub(workspaceFolder, 'getWorkspaceFolder'); + }); + teardown(() => { + sinon.restore(); }); test("getDevelopmentIniPath should return undefined if file doesn't exist", async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - when(fs.fileExists(managePyPath)).thenResolve(false); - - const file = await provider.getDevelopmentIniPath(folder); + pathExistsStub.withArgs(managePyPath).resolves(false); + const file = await pyramidLaunch.getDevelopmentIniPath(folder); expect(file).to.be.equal(undefined, 'Should return undefined'); }); test('getDevelopmentIniPath should file path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - - when(pathUtils.separator).thenReturn('-'); - when(fs.fileExists(managePyPath)).thenResolve(true); - - const file = await provider.getDevelopmentIniPath(folder); + pathSeparatorStub.value('-'); + pathExistsStub.withArgs(managePyPath).resolves(true); + const file = await pyramidLaunch.getDevelopmentIniPath(folder); expect(file).to.be.equal('${workspaceFolder}-development.ini'); }); test('Resolve variables (with resource)', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); - - const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); + workspaceStub.returns(folder); + const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); }); test('Validation of path should return errors if path is undefined', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateIniPath(folder, ''); + const error = await pyramidLaunch.validateIniPath(folder, ''); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if path is empty', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateIniPath(folder, '', ''); + const error = await pyramidLaunch.validateIniPath(folder, '', ''); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if resolved path is empty', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => ''; - - const error = await provider.validateIniPath(folder, '', 'x'); + const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); expect(error).to.be.length.greaterThan(1); }); test("Validation of path should return errors if resolved path doesn't exist", async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz'; - - when(fs.fileExists('xyz')).thenResolve(false); - const error = await provider.validateIniPath(folder, '', 'x'); + pathExistsStub.withArgs('xyz').resolves(false); + const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); expect(error).to.be.length.greaterThan(1); }); test('Validation of path should return errors if resolved path is non-ini', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.txt'; - - when(fs.fileExists('xyz.txt')).thenResolve(true); - const error = await provider.validateIniPath(folder, '', 'x'); + pathExistsStub.withArgs('xyz.txt').resolves(true); + const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); expect(error).to.be.length.greaterThan(1); }); - test('Validation of path should return errors if resolved path is ini', async () => { + test('Validation of path should not return errors if resolved path is ini', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.ini'; - - when(fs.fileExists('xyz.ini')).thenResolve(true); - const error = await provider.validateIniPath(folder, '', 'x'); + pathExistsStub.withArgs('xyz.ini').resolves(true); + const error = await pyramidLaunch.validateIniPath(folder, '', 'xyz.ini'); expect(error).to.be.equal(undefined, 'should not have errors'); }); test('Launch JSON with valid ini path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getDevelopmentIniPath = () => Promise.resolve('xyz.ini'); - when(pathUtils.separator).thenReturn('-'); + pathSeparatorStub.value('-'); - await provider.buildConfiguration(instance(input), state); + await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); const config = { name: DebugConfigStrings.pyramid.snippet.name, type: DebuggerTypeName, request: 'launch', module: 'pyramid.scripts.pserve', - args: ['xyz.ini'], + args: ['${workspaceFolder}-development.ini'], pyramid: true, jinja: true, justMyCode: true, @@ -146,11 +118,10 @@ suite('Debugging - Configuration Provider Pyramid', () => { test('Launch JSON with selected ini path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getDevelopmentIniPath = () => Promise.resolve(undefined); - when(pathUtils.separator).thenReturn('-'); + pathSeparatorStub.value('-'); when(input.showInputBox(anything())).thenResolve('hello'); - await provider.buildConfiguration(instance(input), state); + await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); const config = { name: DebugConfigStrings.pyramid.snippet.name, @@ -168,14 +139,13 @@ suite('Debugging - Configuration Provider Pyramid', () => { test('Launch JSON with default ini path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - provider.getDevelopmentIniPath = () => Promise.resolve(undefined); const workspaceFolderToken = '${workspaceFolder}'; const defaultIni = `${workspaceFolderToken}-development.ini`; - when(pathUtils.separator).thenReturn('-'); + pathSeparatorStub.value('-'); when(input.showInputBox(anything())).thenResolve(); - await provider.buildConfiguration(instance(input), state); + await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); const config = { name: DebugConfigStrings.pyramid.snippet.name, diff --git a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts index 7bf88543bdad..323cda94a1eb 100644 --- a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts @@ -5,34 +5,29 @@ import { expect } from 'chai'; import * as path from 'path'; +import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { RemoteAttachDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; +import * as configuration from '../../../../../client/debugger/extension/configuration/utils/configuration'; +import * as remoteAttach from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import { AttachRequestArguments } from '../../../../../client/debugger/types'; suite('Debugging - Configuration Provider Remote Attach', () => { - let provider: TestRemoteAttachDebugConfigurationProvider; let input: MultiStepInput; - class TestRemoteAttachDebugConfigurationProvider extends RemoteAttachDebugConfigurationProvider { - public async configurePort( - i: MultiStepInput, - config: Partial, - ) { - return super.configurePort(i, config); - } - } + setup(() => { input = mock>(MultiStepInput); - provider = new TestRemoteAttachDebugConfigurationProvider(); + }); + teardown(() => { + sinon.restore(); }); test('Configure port will display prompt', async () => { when(input.showInputBox(anything())).thenResolve(); - await provider.configurePort(instance(input), {}); + await configuration.configurePort(instance(input), {}); verify(input.showInputBox(anything())).once(); }); @@ -40,7 +35,7 @@ suite('Debugging - Configuration Provider Remote Attach', () => { const config: { connect?: { port?: number } } = {}; when(input.showInputBox(anything())).thenResolve('xyz'); - await provider.configurePort(instance(input), config); + await configuration.configurePort(instance(input), config); verify(input.showInputBox(anything())).once(); expect(config).to.be.deep.equal({ connect: { port: 5678 } }); @@ -49,7 +44,7 @@ suite('Debugging - Configuration Provider Remote Attach', () => { const config: { connect?: { port?: number } } = {}; when(input.showInputBox(anything())).thenResolve(); - await provider.configurePort(instance(input), config); + await configuration.configurePort(instance(input), config); verify(input.showInputBox(anything())).once(); expect(config).to.be.deep.equal({ connect: { port: 5678 } }); @@ -58,7 +53,7 @@ suite('Debugging - Configuration Provider Remote Attach', () => { const config: { connect?: { port?: number } } = {}; when(input.showInputBox(anything())).thenResolve('1234'); - await provider.configurePort(instance(input), config); + await configuration.configurePort(instance(input), config); verify(input.showInputBox(anything())).once(); expect(config).to.be.deep.equal({ connect: { port: 1234 } }); @@ -68,12 +63,12 @@ suite('Debugging - Configuration Provider Remote Attach', () => { const state = { config: {}, folder }; let portConfigured = false; when(input.showInputBox(anything())).thenResolve(); - provider.configurePort = () => { + + sinon.stub(configuration, 'configurePort').callsFake(async () => { portConfigured = true; - return Promise.resolve(); - }; + }); - const configurePort = await provider.buildConfiguration(instance(input), state); + const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); if (configurePort) { await configurePort!(input, state); } @@ -103,13 +98,11 @@ suite('Debugging - Configuration Provider Remote Attach', () => { const state = { config: {}, folder }; let portConfigured = false; when(input.showInputBox(anything())).thenResolve('Hello'); - provider.configurePort = (_, cfg) => { + sinon.stub(configuration, 'configurePort').callsFake(async (_, cfg) => { portConfigured = true; cfg.connect!.port = 9999; - return Promise.resolve(); - }; - - const configurePort = await provider.buildConfiguration(instance(input), state); + }); + const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); if (configurePort) { await configurePort(input, state); } diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 6dc27ba2ed9c..28ae70d7bf98 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -17,31 +17,16 @@ import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; import { LaunchJsonReader } from '../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService'; -import { DjangoLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/djangoLaunch'; -import { FastAPILaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/fastapiLaunch'; -import { FileLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/fileLaunch'; -import { FlaskLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/flaskLaunch'; -import { ModuleLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/moduleLaunch'; -import { PidAttachDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/pidAttach'; -import { DebugConfigurationProviderFactory } from '../../../client/debugger/extension/configuration/providers/providerFactory'; -import { PyramidLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/pyramidLaunch'; -import { RemoteAttachDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/remoteAttach'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { - IDebugConfigurationProviderFactory, - IDebugConfigurationResolver, - ILaunchJsonReader, -} from '../../../client/debugger/extension/configuration/types'; +import { IDebugConfigurationResolver, ILaunchJsonReader } from '../../../client/debugger/extension/configuration/types'; import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; import { ChildProcessAttachEventHandler } from '../../../client/debugger/extension/hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from '../../../client/debugger/extension/hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../../client/debugger/extension/hooks/types'; import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; import { - DebugConfigurationType, IDebugAdapterDescriptorFactory, - IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner, IDebugSessionLoggingFactory, @@ -123,69 +108,6 @@ suite('Debugging - Service Registry', () => { 'attach', ), ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProviderFactory, - DebugConfigurationProviderFactory, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - FileLaunchDebugConfigurationProvider, - DebugConfigurationType.launchFile, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - DjangoLaunchDebugConfigurationProvider, - DebugConfigurationType.launchDjango, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - FastAPILaunchDebugConfigurationProvider, - DebugConfigurationType.launchFastAPI, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - FlaskLaunchDebugConfigurationProvider, - DebugConfigurationType.launchFlask, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - RemoteAttachDebugConfigurationProvider, - DebugConfigurationType.remoteAttach, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - ModuleLaunchDebugConfigurationProvider, - DebugConfigurationType.launchModule, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - PyramidLaunchDebugConfigurationProvider, - DebugConfigurationType.launchPyramid, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationProvider, - PidAttachDebugConfigurationProvider, - DebugConfigurationType.pidAttach, - ), - ).once(); - verify( serviceManager.addSingleton( IExtensionSingleActivationService, From d44e8771e428f94a3fe76b68a0927298ea20eb34 Mon Sep 17 00:00:00 2001 From: ChinazoOnwukaike <90980092+ChinazoOnwukaike@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:50:44 -0500 Subject: [PATCH 38/59] Remove code lenses for "set as interpreter" (#19853) Closes https://github.com/microsoft/vscode-python/issues/3854 Removed shebangCodeLensProvider and IShebangCodeLensProvider as well as their references and instances. Also removed the setShebangInterpreter. --- .eslintignore | 1 - build/existingFiles.json | 1 - .../commands/setShebangInterpreter.ts | 75 ------ src/client/interpreter/contracts.ts | 7 +- .../display/shebangCodeLensProvider.ts | 84 ------- src/client/interpreter/serviceRegistry.ts | 9 +- .../interpreters/serviceRegistry.unit.test.ts | 11 +- .../shebangCodeLenseProvider.unit.test.ts | 228 ------------------ 8 files changed, 3 insertions(+), 413 deletions(-) delete mode 100644 src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts delete mode 100644 src/client/interpreter/display/shebangCodeLensProvider.ts delete mode 100644 src/test/providers/shebangCodeLenseProvider.unit.test.ts diff --git a/.eslintignore b/.eslintignore index 20f49860c5a0..2f759e89c516 100644 --- a/.eslintignore +++ b/.eslintignore @@ -158,7 +158,6 @@ src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts src/client/interpreter/helpers.ts src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts src/client/interpreter/activation/service.ts -src/client/interpreter/display/shebangCodeLensProvider.ts src/client/interpreter/display/index.ts src/client/api.ts diff --git a/build/existingFiles.json b/build/existingFiles.json index bb0c31c8b159..0d7e0c3c41cc 100644 --- a/build/existingFiles.json +++ b/build/existingFiles.json @@ -170,7 +170,6 @@ "src/client/interpreter/configuration/types.ts", "src/client/interpreter/contracts.ts", "src/client/interpreter/display/index.ts", - "src/client/interpreter/display/shebangCodeLensProvider.ts", "src/client/interpreter/helpers.ts", "src/client/interpreter/interpreterService.ts", "src/client/interpreter/interpreterVersion.ts", diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts deleted file mode 100644 index 94ec84b82c42..000000000000 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ConfigurationTarget } from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; -import { Commands } from '../../../../common/constants'; -import { IDisposableRegistry } from '../../../../common/types'; -import { IShebangCodeLensProvider } from '../../../contracts'; -import { IPythonPathUpdaterServiceManager } from '../../types'; - -@injectable() -export class SetShebangInterpreterCommand implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - constructor( - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IPythonPathUpdaterServiceManager) - private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, - @inject(IShebangCodeLensProvider) private readonly shebangCodeLensProvider: IShebangCodeLensProvider, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - ) {} - - public async activate() { - this.disposables.push( - this.commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this)), - ); - } - - protected async setShebangInterpreter(): Promise { - const shebang = await this.shebangCodeLensProvider.detectShebang( - this.documentManager.activeTextEditor!.document, - true, - ); - if (!shebang) { - return; - } - - const isGlobalChange = - !Array.isArray(this.workspaceService.workspaceFolders) || - this.workspaceService.workspaceFolders.length === 0; - const workspaceFolder = this.workspaceService.getWorkspaceFolder( - this.documentManager.activeTextEditor!.document.uri, - ); - const isWorkspaceChange = - Array.isArray(this.workspaceService.workspaceFolders) && - this.workspaceService.workspaceFolders.length === 1; - - if (isGlobalChange) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); - return; - } - - if (isWorkspaceChange || !workspaceFolder) { - await this.pythonPathUpdaterService.updatePythonPath( - shebang, - ConfigurationTarget.Workspace, - 'shebang', - this.workspaceService.workspaceFolders![0].uri, - ); - return; - } - - await this.pythonPathUpdaterService.updatePythonPath( - shebang, - ConfigurationTarget.WorkspaceFolder, - 'shebang', - workspaceFolder.uri, - ); - } -} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 8f31bde11cec..a79a5250ec99 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,5 +1,5 @@ import { SemVer } from 'semver'; -import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode'; +import { ConfigurationTarget, Disposable, Event, Uri } from 'vscode'; import { FileChangeType } from '../common/platform/fileSystemWatcher'; import { Resource } from '../common/types'; import { PythonEnvSource } from '../pythonEnvironments/base/info'; @@ -100,11 +100,6 @@ export interface IInterpreterDisplay { registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter): void; } -export const IShebangCodeLensProvider = Symbol('IShebangCodeLensProvider'); -export interface IShebangCodeLensProvider extends CodeLensProvider { - detectShebang(document: TextDocument, resolveShebangAsInterpreter?: boolean): Promise; -} - export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined; diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts deleted file mode 100644 index 59f531c5fa73..000000000000 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { CancellationToken, CodeLens, Command, Event, Position, Range, TextDocument, Uri } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; -import { arePathsSame } from '../../common/platform/fs-paths'; -import { IPlatformService } from '../../common/platform/types'; -import * as internalPython from '../../common/process/internal/python'; -import { IProcessServiceFactory } from '../../common/process/types'; -import { IInterpreterService, IShebangCodeLensProvider } from '../contracts'; - -@injectable() -export class ShebangCodeLensProvider implements IShebangCodeLensProvider { - public readonly onDidChangeCodeLenses: Event; - constructor( - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - ) { - this.onDidChangeCodeLenses = (workspaceService.onDidChangeConfiguration as any) as Event; - } - public async detectShebang( - document: TextDocument, - resolveShebangAsInterpreter: boolean = false, - ): Promise { - const firstLine = document.lineAt(0); - if (firstLine.isEmptyOrWhitespace) { - return; - } - - if (!firstLine.text.startsWith('#!')) { - return; - } - - const shebang = firstLine.text.substr(2).trim(); - if (resolveShebangAsInterpreter) { - const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang, document.uri); - return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; - } else { - return typeof shebang === 'string' && shebang.length > 0 ? shebang : undefined; - } - } - public async provideCodeLenses(document: TextDocument, _token?: CancellationToken): Promise { - return this.createShebangCodeLens(document); - } - private async getFullyQualifiedPathToInterpreter(pythonPath: string, resource: Uri) { - let cmdFile = pythonPath; - const [args, parse] = internalPython.getExecutable(); - if (pythonPath.indexOf('bin/env ') >= 0 && !this.platformService.isWindows) { - // In case we have pythonPath as '/usr/bin/env python'. - const parts = pythonPath - .split(' ') - .map((part) => part.trim()) - .filter((part) => part.length > 0); - cmdFile = parts.shift()!; - args.splice(0, 0, ...parts); - } - const processService = await this.processServiceFactory.create(resource); - return processService - .exec(cmdFile, args) - .then((output) => parse(output.stdout)) - .catch(() => ''); - } - private async createShebangCodeLens(document: TextDocument) { - const shebang = await this.detectShebang(document); - if (!shebang) { - return []; - } - const interpreter = await this.interpreterService.getActiveInterpreter(document.uri); - if (interpreter && arePathsSame(shebang, interpreter.path)) { - return []; - } - const firstLine = document.lineAt(0); - const startOfShebang = new Position(0, 0); - const endOfShebang = new Position(0, firstLine.text.length - 1); - const shebangRange = new Range(startOfShebang, endOfShebang); - - const cmd: Command = { - command: 'python.setShebangInterpreter', - title: 'Set as interpreter', - }; - - return [new CodeLens(shebangRange, cmd)]; - } -} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index cb60c370b84f..cdcd8718fd1d 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -15,7 +15,6 @@ import { InstallPythonCommand } from './configuration/interpreterSelector/comman import { InstallPythonViaTerminal } from './configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter'; import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter'; -import { SetShebangInterpreterCommand } from './configuration/interpreterSelector/commands/setShebangInterpreter'; import { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; @@ -26,10 +25,9 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, } from './configuration/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, IShebangCodeLensProvider } from './contracts'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts'; import { InterpreterDisplay } from './display'; import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; -import { ShebangCodeLensProvider } from './display/shebangCodeLensProvider'; import { InterpreterHelper } from './helpers'; import { InterpreterService } from './interpreterService'; import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; @@ -59,10 +57,6 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IExtensionSingleActivationService, ResetInterpreterCommand, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - SetShebangInterpreterCommand, - ); serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); @@ -81,7 +75,6 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void ); serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); - serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); serviceManager.addSingleton(IInterpreterComparer, EnvironmentTypeComparer); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index a6fdce77dd3e..dff756cd3e64 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -18,7 +18,6 @@ import { InstallPythonCommand } from '../../client/interpreter/configuration/int import { InstallPythonViaTerminal } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; import { ResetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter'; import { SetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; -import { SetShebangInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter'; import { InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; @@ -29,15 +28,9 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, } from '../../client/interpreter/configuration/types'; -import { - IInterpreterDisplay, - IInterpreterHelper, - IInterpreterService, - IShebangCodeLensProvider, -} from '../../client/interpreter/contracts'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { InterpreterLocatorProgressStatubarHandler } from '../../client/interpreter/display/progressDisplay'; -import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; import { InterpreterHelper } from '../../client/interpreter/helpers'; import { InterpreterService } from '../../client/interpreter/interpreterService'; import { registerTypes } from '../../client/interpreter/serviceRegistry'; @@ -56,7 +49,6 @@ suite('Interpreters - Service Registry', () => { [IExtensionSingleActivationService, SetInterpreterCommand], [IInterpreterQuickPick, SetInterpreterCommand], [IExtensionSingleActivationService, ResetInterpreterCommand], - [IExtensionSingleActivationService, SetShebangInterpreterCommand], [IExtensionActivationService, VirtualEnvironmentPrompt], @@ -66,7 +58,6 @@ suite('Interpreters - Service Registry', () => { [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], [IInterpreterSelector, InterpreterSelector], - [IShebangCodeLensProvider, ShebangCodeLensProvider], [IInterpreterHelper, InterpreterHelper], [IInterpreterComparer, EnvironmentTypeComparer], diff --git a/src/test/providers/shebangCodeLenseProvider.unit.test.ts b/src/test/providers/shebangCodeLenseProvider.unit.test.ts deleted file mode 100644 index d62bcef4ab8b..000000000000 --- a/src/test/providers/shebangCodeLenseProvider.unit.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { TextDocument, TextLine, Uri } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { PlatformService } from '../../client/common/platform/platformService'; -import { IPlatformService } from '../../client/common/platform/types'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; -import { PythonEnvironment } from '../../client/pythonEnvironments/info'; - -suite('Shebang detection', () => { - let interpreterService: IInterpreterService; - let workspaceService: IWorkspaceService; - let provider: ShebangCodeLensProvider; - let factory: IProcessServiceFactory; - let processService: typemoq.IMock; - let platformService: typemoq.IMock; - setup(() => { - interpreterService = mock(); - workspaceService = mock(WorkspaceService); - factory = mock(ProcessServiceFactory); - processService = typemoq.Mock.ofType(); - platformService = typemoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - when(factory.create(anything())).thenResolve(processService.object); - provider = new ShebangCodeLensProvider( - instance(factory), - instance(interpreterService), - platformService.object, - instance(workspaceService), - ); - }); - function createDocument( - firstLine: string, - uri = Uri.parse('xyz.py'), - ): [typemoq.IMock, typemoq.IMock] { - const doc = typemoq.Mock.ofType(); - const line = typemoq.Mock.ofType(); - - line.setup((l) => l.isEmptyOrWhitespace) - .returns(() => firstLine.length === 0) - .verifiable(typemoq.Times.once()); - line.setup((l) => l.text).returns(() => firstLine); - - doc.setup((d) => d.lineAt(typemoq.It.isValue(0))) - .returns(() => line.object) - .verifiable(typemoq.Times.once()); - doc.setup((d) => d.uri).returns(() => uri); - - return [doc, line]; - } - test('Shebang should be empty when first line is empty when resolving shebang as interpreter', async () => { - const [document, line] = createDocument(''); - - const shebang = await provider.detectShebang(document.object, true); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); - }); - test('Shebang should be empty when first line is empty when not resolving shebang as interpreter', async () => { - const [document, line] = createDocument(''); - - const shebang = await provider.detectShebang(document.object, false); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); - }); - test('Shebang should be returned as it is when not resolving shebang as interpreter', async () => { - const [document, line] = createDocument('#!HELLO'); - - const shebang = await provider.detectShebang(document.object, false); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal('HELLO', 'Shebang should be HELLO'); - }); - test('Shebang should be empty when python path is invalid in shebang', async () => { - const [document, line] = createDocument('#!HELLO'); - - processService - .setup((p) => p.exec(typemoq.It.isValue('HELLO'), typemoq.It.isAny())) - .returns(() => Promise.reject()) - .verifiable(typemoq.Times.once()); - - const shebang = await provider.detectShebang(document.object, true); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); - processService.verifyAll(); - }); - test('Shebang should be returned when python path is valid', async () => { - const [document, line] = createDocument('#!HELLO'); - - processService - .setup((p) => p.exec(typemoq.It.isValue('HELLO'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'THIS_IS_IT' })) - .verifiable(typemoq.Times.once()); - - const shebang = await provider.detectShebang(document.object, true); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal('THIS_IS_IT'); - processService.verifyAll(); - }); - test("Shebang should be returned when python path is valid and text is'/usr/bin/env python'", async () => { - const [document, line] = createDocument('#!/usr/bin/env python'); - platformService - .setup((p) => p.isWindows) - .returns(() => false) - .verifiable(typemoq.Times.once()); - processService - .setup((p) => p.exec(typemoq.It.isValue('/usr/bin/env'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'THIS_IS_IT' })) - .verifiable(typemoq.Times.once()); - - const shebang = await provider.detectShebang(document.object, true); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal('THIS_IS_IT'); - processService.verifyAll(); - platformService.verifyAll(); - }); - test("Shebang should be returned when python path is valid and text is'/usr/bin/env python' and is windows", async () => { - const [document, line] = createDocument('#!/usr/bin/env python'); - platformService - .setup((p) => p.isWindows) - .returns(() => true) - .verifiable(typemoq.Times.once()); - processService - .setup((p) => p.exec(typemoq.It.isValue('/usr/bin/env python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'THIS_IS_IT' })) - .verifiable(typemoq.Times.once()); - - const shebang = await provider.detectShebang(document.object, true); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal('THIS_IS_IT'); - processService.verifyAll(); - platformService.verifyAll(); - }); - - test("No code lens when there's no shebang", async () => { - const [document] = createDocument(''); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - path: 'python', - } as unknown) as PythonEnvironment); - processService - .setup((p) => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'python' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve(''); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(0); - }); - test('No code lens when shebang is an empty string', async () => { - const [document] = createDocument('#!'); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - path: 'python', - } as unknown) as PythonEnvironment); - processService - .setup((p) => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'python' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve(''); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(0); - }); - test('No code lens when python path in settings is the same as that in shebang', async () => { - const [document] = createDocument('#!python'); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - path: 'python', - } as unknown) as PythonEnvironment); - processService - .setup((p) => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'python' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve('python'); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(0); - }); - test('Code lens returned when python path in settings is different to one in shebang', async () => { - const [document] = createDocument('#!python'); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - path: 'different', - } as unknown) as PythonEnvironment); - processService - .setup((p) => p.exec(typemoq.It.isValue('different'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'different' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve('python'); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(1); - expect(codeLenses[0].command!.command).to.equal('python.setShebangInterpreter'); - expect(codeLenses[0].command!.title).to.equal('Set as interpreter'); - expect(codeLenses[0].range.start.character).to.equal(0); - expect(codeLenses[0].range.start.line).to.equal(0); - expect(codeLenses[0].range.end.line).to.equal(0); - }); -}); From 32c9adc5c7de840f0df18922d423c7ee39ab05b2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 19 Sep 2022 17:26:02 -0700 Subject: [PATCH 39/59] Add type info which can be used to differentiate Pyenv global installs from Pyenv virtualenvs (#19862) --- .../pythonEnvironments/base/info/env.ts | 6 ++++ .../pythonEnvironments/base/info/index.ts | 6 ++++ .../base/locators/composite/resolverUtils.ts | 33 +++++++++++++++++-- .../environmentManagers/simplevirtualenvs.ts | 9 +++++ .../composite/envsResolver.unit.test.ts | 13 ++++++++ .../composite/resolverUtils.unit.test.ts | 6 ++++ 6 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index e65339b78c27..695e8e706c23 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -16,6 +16,7 @@ import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, + PythonEnvType, PythonReleaseLevel, PythonVersion, virtualEnvKinds, @@ -40,6 +41,7 @@ export function buildEnvInfo(init?: { display?: string; sysPrefix?: string; searchLocation?: Uri; + type?: PythonEnvType; }): PythonEnvInfo { const env: PythonEnvInfo = { name: init?.name ?? '', @@ -103,6 +105,7 @@ function updateEnv( location?: string; version?: PythonVersion; searchLocation?: Uri; + type?: PythonEnvType; }, ): void { if (updates.kind !== undefined) { @@ -120,6 +123,9 @@ function updateEnv( if (updates.searchLocation !== undefined) { env.searchLocation = updates.searchLocation; } + if (updates.type !== undefined) { + env.type = updates.type; + } } /** diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 13d0b29a96e8..4ef512c56ed6 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -26,6 +26,11 @@ export enum PythonEnvKind { OtherVirtual = 'virt-other', } +export enum PythonEnvType { + Conda = 'Conda', + Virtual = 'Virtual', +} + export interface EnvPathType { /** * Path to environment folder or path to interpreter that uniquely identifies an environment. @@ -105,6 +110,7 @@ export enum PythonEnvSource { type PythonEnvBaseInfo = { id?: string; kind: PythonEnvKind; + type?: PythonEnvType; executable: PythonExecutableInfo; // One of (name, location) must be non-empty. name: string; diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c41c52510280..a8ad85b05540 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -4,7 +4,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { uniq } from 'lodash'; -import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, UNKNOWN_PYTHON_VERSION, virtualEnvKinds } from '../../info'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + UNKNOWN_PYTHON_VERSION, + virtualEnvKinds, +} from '../../info'; import { buildEnvInfo, comparePythonVersionSpecificity, @@ -26,6 +33,7 @@ import { getRegistryInterpreters, getRegistryInterpretersSync } from '../../../c import { BasicEnvInfo } from '../../locator'; import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; +import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -62,9 +70,26 @@ export async function resolveBasicEnv(env: BasicEnvInfo, useCache = false): Prom const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); resolvedEnv.executable.ctime = ctime; resolvedEnv.executable.mtime = mtime; + const type = await getEnvType(resolvedEnv); + if (type) { + resolvedEnv.type = type; + } return resolvedEnv; } +async function getEnvType(env: PythonEnvInfo) { + if (env.type) { + return env.type; + } + if (await isVirtualEnvironment(env.executable.filename)) { + return PythonEnvType.Virtual; + } + if (await isCondaEnvironment(env.executable.filename)) { + return PythonEnvType.Conda; + } + return undefined; +} + function getSearchLocation(env: PythonEnvInfo): Uri | undefined { const folders = getWorkspaceFolders(); const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f)); @@ -131,6 +156,7 @@ async function resolveSimpleEnv(env: BasicEnvInfo): Promise { kind, version: await getPythonVersionFromPath(executablePath), executable: executablePath, + type: PythonEnvType.Virtual, }); const location = getEnvironmentDirFromPath(executablePath); envInfo.location = location; @@ -161,6 +187,7 @@ async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise

{ diff --git a/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts index 915bc8950a01..80a60a0580ca 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts @@ -30,6 +30,15 @@ function getPyvenvConfigPathsFrom(interpreterPath: string): string[] { return [venvPath1, venvPath2]; } +/** + * Checks if the given interpreter is a virtual environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isVirtualEnvironment(interpreterPath: string): Promise { + return isVenvEnvironment(interpreterPath); +} + /** * Checks if the given interpreter belongs to a venv based environment. * @param {string} interpreterPath: Absolute path to the python interpreter. diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts index 158dab04e875..f0c1a7d022f0 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -13,6 +13,7 @@ import * as platformApis from '../../../../../client/common/utils/platform'; import { PythonEnvInfo, PythonEnvKind, + PythonEnvType, PythonVersion, UNKNOWN_PYTHON_VERSION, } from '../../../../../client/pythonEnvironments/base/info'; @@ -66,6 +67,9 @@ suite('Python envs locator - Environments Resolver', () => { updatedEnv.arch = Architecture.x64; updatedEnv.display = expectedDisplay; updatedEnv.detailedDisplayName = expectedDisplay; + if (env.kind === PythonEnvKind.Conda) { + env.type = PythonEnvType.Conda; + } return updatedEnv; } @@ -76,6 +80,7 @@ suite('Python envs locator - Environments Resolver', () => { name = '', location = '', display: string | undefined = undefined, + type?: PythonEnvType, ): PythonEnvInfo { return { name, @@ -94,6 +99,7 @@ suite('Python envs locator - Environments Resolver', () => { distro: { org: '' }, searchLocation: Uri.file(location), source: [], + type, }; } suite('iterEnvs()', () => { @@ -128,6 +134,7 @@ suite('Python envs locator - Environments Resolver', () => { 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), "Python ('win1': venv)", + PythonEnvType.Virtual, ); const envsReturnedByParentLocator = [env1]; const parentLocator = new SimpleLocator(envsReturnedByParentLocator); @@ -151,6 +158,8 @@ suite('Python envs locator - Environments Resolver', () => { undefined, 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, ); const envsReturnedByParentLocator = [env1]; const parentLocator = new SimpleLocator(envsReturnedByParentLocator); @@ -206,6 +215,8 @@ suite('Python envs locator - Environments Resolver', () => { undefined, 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, ); const envsReturnedByParentLocator = [env]; const didUpdate = new EventEmitter | ProgressNotificationEvent>(); @@ -355,6 +366,8 @@ suite('Python envs locator - Environments Resolver', () => { undefined, 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, ); const parentLocator = new SimpleLocator([]); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts index 893f8bed7655..322a4cb8cc91 100644 --- a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -11,6 +11,7 @@ import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, + PythonEnvType, PythonVersion, UNKNOWN_PYTHON_VERSION, } from '../../../../../client/pythonEnvironments/base/info'; @@ -76,6 +77,7 @@ suite('Resolver Utils', () => { }, source: [], org: 'miniconda3', + type: PythonEnvType.Conda, }); envInfo.location = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12'); envInfo.name = 'base'; @@ -209,6 +211,7 @@ suite('Resolver Utils', () => { version: UNKNOWN_PYTHON_VERSION, fileInfo: undefined, name: 'base', + type: PythonEnvType.Conda, }); setEnvDisplayString(info); return info; @@ -237,6 +240,7 @@ suite('Resolver Utils', () => { searchLocation: undefined, source: [], }; + info.type = PythonEnvType.Conda; setEnvDisplayString(info); return info; } @@ -333,6 +337,7 @@ suite('Resolver Utils', () => { distro: { org: '' }, searchLocation: Uri.file(location), source: [], + type: PythonEnvType.Virtual, }; setEnvDisplayString(info); return info; @@ -623,6 +628,7 @@ suite('Resolver Utils', () => { org: 'ContinuumAnalytics', // Provided by registry name: 'conda3', source: [PythonEnvSource.WindowsRegistry], + type: PythonEnvType.Conda, }); setEnvDisplayString(expected); expected.distro.defaultDisplayName = 'Anaconda py38_4.8.3'; From 9dc03b116c53bf319ecbc793f7569f138ce472fe Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Tue, 20 Sep 2022 10:19:39 -0700 Subject: [PATCH 40/59] Update property name under GDPR classification (#19854) To clear property named "errortype" under the "call-tas-error" event. --- src/client/telemetry/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 326b125df1cd..f8c443ff58ce 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1964,7 +1964,7 @@ export interface IEventNamePropertyMapping { "call-tas-error" : { "owner": "luabud", "comment": "Logs when calls to the experiment service fails", - "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth"} + "errortype": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Type of error when calling TAS (ServerError, NoResponse, etc.)"} } */ } From 192c3eabd8a065492f237196b052145364e68cb4 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 23 Sep 2022 14:57:44 -0700 Subject: [PATCH 41/59] Create environment using `venv` or `conda` (#19848) Closes #19676 Closes #19850 --- package.json | 11 + package.nls.json | 5 +- pythonFiles/create_conda.py | 128 +++++++++ pythonFiles/create_venv.py | 130 +++++++++ pythonFiles/tests/test_create_conda.py | 72 +++++ pythonFiles/tests/test_create_venv.py | 104 +++++++ src/client/common/constants.ts | 39 +-- .../common/process/internal/scripts/index.ts | 10 + src/client/common/utils/localize.ts | 71 +++++ src/client/common/vscodeApis/commandApis.ts | 15 + src/client/common/vscodeApis/windowApis.ts | 46 +++ src/client/common/vscodeApis/workspaceApis.ts | 12 + src/client/extension.ts | 4 +- src/client/extensionActivation.ts | 9 + .../base/locators/composite/resolverUtils.ts | 5 +- .../common/externalDependencies.ts | 4 - .../creation/common/commonUtils.ts | 13 + .../creation/common/workspaceSelection.ts | 67 +++++ .../creation/createEnvApi.ts | 60 ++++ .../creation/createEnvQuickPick.ts | 54 ++++ .../creation/createEnvironment.ts | 47 ++++ .../provider/condaCreationProvider.ts | 189 +++++++++++++ .../creation/provider/condaUtils.ts | 33 +++ .../creation/provider/venvCreationProvider.ts | 156 ++++++++++ .../pythonEnvironments/creation/types.ts | 22 ++ .../composite/envsResolver.unit.test.ts | 5 +- .../composite/resolverUtils.unit.test.ts | 3 +- .../common/workspaceSelection.unit.test.ts | 169 +++++++++++ .../creation/createEnvQuickPick.unit.test.ts | 69 +++++ .../creation/createEnvironment.unit.test.ts | 70 +++++ .../condaCreationProvider.unit.test.ts | 150 ++++++++++ .../venvCreationProvider.unit.test.ts | 266 ++++++++++++++++++ 32 files changed, 2007 insertions(+), 31 deletions(-) create mode 100644 pythonFiles/create_conda.py create mode 100644 pythonFiles/create_venv.py create mode 100644 pythonFiles/tests/test_create_conda.py create mode 100644 pythonFiles/tests/test_create_venv.py create mode 100644 src/client/common/vscodeApis/commandApis.ts create mode 100644 src/client/common/vscodeApis/windowApis.ts create mode 100644 src/client/common/vscodeApis/workspaceApis.ts create mode 100644 src/client/pythonEnvironments/creation/common/commonUtils.ts create mode 100644 src/client/pythonEnvironments/creation/common/workspaceSelection.ts create mode 100644 src/client/pythonEnvironments/creation/createEnvApi.ts create mode 100644 src/client/pythonEnvironments/creation/createEnvQuickPick.ts create mode 100644 src/client/pythonEnvironments/creation/createEnvironment.ts create mode 100644 src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts create mode 100644 src/client/pythonEnvironments/creation/provider/condaUtils.ts create mode 100644 src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts create mode 100644 src/client/pythonEnvironments/creation/types.ts create mode 100644 src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts diff --git a/package.json b/package.json index 20a39d429eb9..1d7a09308eed 100644 --- a/package.json +++ b/package.json @@ -271,6 +271,11 @@ "command": "python.createTerminal", "title": "%python.command.python.createTerminal.title%" }, + { + "category": "Python", + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%" + }, { "category": "Python", "command": "python.enableLinting", @@ -1540,6 +1545,12 @@ "title": "%python.command.python.configureTests.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, + { + "category": "Python", + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%", + "when": "!virtualWorkspace && shellExecutionSupported" + }, { "category": "Python", "command": "python.createTerminal", diff --git a/package.nls.json b/package.nls.json index 611c98ed85e2..7a39d545fab6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,6 +1,7 @@ { "python.command.python.sortImports.title": "Sort Imports", "python.command.python.startREPL.title": "Start REPL", + "python.command.python.createEnvironment.title": "Create Environment", "python.command.python.createTerminal.title": "Create Terminal", "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", @@ -24,7 +25,7 @@ "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.menu.createNewFile.title": "Python File", - "python.autoComplete.extraPaths.description":"List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", + "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See https://aka.ms/AAfekmf to understand when this is used", "python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.", @@ -32,7 +33,7 @@ "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", "python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", "python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", - "python.formatting.autopep8Args.description":"Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", diff --git a/pythonFiles/create_conda.py b/pythonFiles/create_conda.py new file mode 100644 index 000000000000..0e48ee6b2286 --- /dev/null +++ b/pythonFiles/create_conda.py @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import os +import pathlib +import subprocess +import sys +from typing import Optional, Sequence, Union + +CONDA_ENV_NAME = ".conda" +CWD = pathlib.PurePath(os.getcwd()) + + +class VenvError(Exception): + pass + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--python", + action="store", + help="Python version to install in the virtual environment.", + default=f"{sys.version_info.major}.{sys.version_info.minor}", + ) + parser.add_argument( + "--install", + action="store_true", + default=False, + help="Install packages into the virtual environment.", + ) + parser.add_argument( + "--git-ignore", + action="store_true", + default=False, + help="Add .gitignore to the newly created virtual environment.", + ) + parser.add_argument( + "--name", + default=CONDA_ENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + return parser.parse_args(argv) + + +def file_exists(path: Union[str, pathlib.PurePath]) -> bool: + return os.path.exists(path) + + +def conda_env_exists(name: Union[str, pathlib.PurePath]) -> bool: + return os.path.exists(CWD / name) + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) + except subprocess.CalledProcessError: + raise VenvError(error_message) + + +def get_conda_env_path(name: str) -> str: + return os.fspath(CWD / name) + + +def install_packages(env_path: str) -> None: + yml = os.fspath(CWD / "environment.yml") + if file_exists(yml): + print(f"CONDA_INSTALLING_YML: {yml}") + run_process( + [ + sys.executable, + "-m", + "conda", + "env", + "update", + "--prefix", + env_path, + "--file", + yml, + ], + "CREATE_CONDA.FAILED_INSTALL_YML", + ) + + +def add_gitignore(name: str) -> None: + git_ignore = os.fspath(CWD / name / ".gitignore") + if not file_exists(git_ignore): + print(f"Creating: {git_ignore}") + with open(git_ignore, "w") as f: + f.write("*") + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + if not conda_env_exists(args.name): + run_process( + [ + sys.executable, + "-m", + "conda", + "create", + "--yes", + "--prefix", + args.name, + f"python={args.python}", + ], + "CREATE_CONDA.ENV_FAILED_CREATION", + ) + if args.git_ignore: + add_gitignore(args.name) + + env_path = get_conda_env_path(args.name) + print(f"CREATED_CONDA_ENV:{env_path}") + + if args.install: + install_packages(env_path) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py new file mode 100644 index 000000000000..4d9b551798e1 --- /dev/null +++ b/pythonFiles/create_venv.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import importlib.util as import_util +import os +import pathlib +import subprocess +import sys +from typing import Optional, Sequence, Union + +VENV_NAME = ".venv" +CWD = pathlib.PurePath(os.getcwd()) + + +class VenvError(Exception): + pass + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--install", + action="store_true", + default=False, + help="Install packages into the virtual environment.", + ) + parser.add_argument( + "--git-ignore", + action="store_true", + default=False, + help="Add .gitignore to the newly created virtual environment.", + ) + parser.add_argument( + "--name", + default=VENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + return parser.parse_args(argv) + + +def is_installed(module: str) -> bool: + return import_util.find_spec(module) is not None + + +def file_exists(path: Union[str, pathlib.PurePath]) -> bool: + return os.path.exists(path) + + +def venv_exists(name: str) -> bool: + return os.path.exists(CWD / name) + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) + except subprocess.CalledProcessError: + raise VenvError(error_message) + + +def get_venv_path(name: str) -> str: + # See `venv` doc here for more details on binary location: + # https://docs.python.org/3/library/venv.html#creating-virtual-environments + if sys.platform == "win32": + return os.fspath(CWD / name / "Scripts" / "python.exe") + else: + return os.fspath(CWD / name / "bin" / "python") + + +def install_packages(venv_path: str) -> None: + if not is_installed("pip"): + raise VenvError("CREATE_VENV.PIP_NOT_FOUND") + + requirements = os.fspath(CWD / "requirements.txt") + pyproject = os.fspath(CWD / "pyproject.toml") + + run_process( + [venv_path, "-m", "pip", "install", "--upgrade", "pip"], + "CREATE_VENV.PIP_UPGRADE_FAILED", + ) + + if file_exists(requirements): + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + run_process( + [venv_path, "-m", "pip", "install", "-r", requirements], + "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", + ) + elif file_exists(pyproject): + print(f"VENV_INSTALLING_PYPROJECT: {pyproject}") + run_process( + [venv_path, "-m", "pip", "install", "-e", ".[extras]"], + "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT", + ) + + +def add_gitignore(name: str) -> None: + git_ignore = CWD / name / ".gitignore" + if not file_exists(git_ignore): + print("Creating: " + os.fspath(git_ignore)) + with open(git_ignore, "w") as f: + f.write("*") + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + if is_installed("venv"): + if not venv_exists(args.name): + run_process( + [sys.executable, "-m", "venv", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) + if args.git_ignore: + add_gitignore(args.name) + venv_path = get_venv_path(args.name) + print(f"CREATED_VENV:{venv_path}") + if args.install: + install_packages(venv_path) + else: + raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/pythonFiles/tests/test_create_conda.py b/pythonFiles/tests/test_create_conda.py new file mode 100644 index 000000000000..29dc323402eb --- /dev/null +++ b/pythonFiles/tests/test_create_conda.py @@ -0,0 +1,72 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import importlib +import sys + +import create_conda +import pytest + + +@pytest.mark.parametrize("env_exists", [True, False]) +@pytest.mark.parametrize("git_ignore", [True, False]) +@pytest.mark.parametrize("install", [True, False]) +@pytest.mark.parametrize("python", [True, False]) +def test_create_env(env_exists, git_ignore, install, python): + importlib.reload(create_conda) + create_conda.conda_env_exists = lambda _n: env_exists + + install_packages_called = False + + def install_packages(_name): + nonlocal install_packages_called + install_packages_called = True + + create_conda.install_packages = install_packages + + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + version = ( + "12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}" + ) + if not env_exists: + assert args == [ + sys.executable, + "-m", + "conda", + "create", + "--yes", + "--prefix", + create_conda.CONDA_ENV_NAME, + f"python={version}", + ] + assert error_message == "CREATE_CONDA.ENV_FAILED_CREATION" + + create_conda.run_process = run_process + + add_gitignore_called = False + + def add_gitignore(_name): + nonlocal add_gitignore_called + add_gitignore_called = True + + create_conda.add_gitignore = add_gitignore + + args = [] + if git_ignore: + args.append("--git-ignore") + if install: + args.append("--install") + if python: + args.extend(["--python", "12345"]) + create_conda.main(args) + assert install_packages_called == install + + # run_process is called when the venv does not exist + assert run_process_called != env_exists + + # add_gitignore is called when new venv is created and git_ignore is True + assert add_gitignore_called == (not env_exists and git_ignore) diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py new file mode 100644 index 000000000000..e002ad17ef95 --- /dev/null +++ b/pythonFiles/tests/test_create_venv.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import importlib +import sys + +import create_venv +import pytest + + +def test_venv_not_installed(): + importlib.reload(create_venv) + create_venv.is_installed = lambda module: module != "venv" + with pytest.raises(create_venv.VenvError) as e: + create_venv.main() + assert str(e.value) == "CREATE_VENV.VENV_NOT_FOUND" + + +def test_pip_not_installed(): + importlib.reload(create_venv) + create_venv.venv_exists = lambda _n: True + create_venv.is_installed = lambda module: module != "pip" + create_venv.run_process = lambda _args, _error_message: None + with pytest.raises(create_venv.VenvError) as e: + create_venv.main(["--install"]) + assert str(e.value) == "CREATE_VENV.PIP_NOT_FOUND" + + +@pytest.mark.parametrize("env_exists", [True, False]) +@pytest.mark.parametrize("git_ignore", [True, False]) +@pytest.mark.parametrize("install", [True, False]) +def test_create_env(env_exists, git_ignore, install): + importlib.reload(create_venv) + create_venv.is_installed = lambda _x: True + create_venv.venv_exists = lambda _n: env_exists + + install_packages_called = False + + def install_packages(_name): + nonlocal install_packages_called + install_packages_called = True + + create_venv.install_packages = install_packages + + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + if not env_exists: + assert args == [sys.executable, "-m", "venv", create_venv.VENV_NAME] + assert error_message == "CREATE_VENV.VENV_FAILED_CREATION" + + create_venv.run_process = run_process + + add_gitignore_called = False + + def add_gitignore(_name): + nonlocal add_gitignore_called + add_gitignore_called = True + + create_venv.add_gitignore = add_gitignore + + args = [] + if git_ignore: + args.append("--git-ignore") + if install: + args.append("--install") + create_venv.main(args) + assert install_packages_called == install + + # run_process is called when the venv does not exist + assert run_process_called != env_exists + + # add_gitignore is called when new venv is created and git_ignore is True + assert add_gitignore_called == (not env_exists and git_ignore) + + +@pytest.mark.parametrize("install_type", ["requirements", "pyproject"]) +def test_install_packages(install_type): + importlib.reload(create_venv) + create_venv.is_installed = lambda _x: True + create_venv.file_exists = lambda x: install_type in x + + pip_upgraded = False + installing = None + + def run_process(args, error_message): + nonlocal pip_upgraded, installing + if args[1:] == ["-m", "pip", "install", "--upgrade", "pip"]: + pip_upgraded = True + assert error_message == "CREATE_VENV.PIP_UPGRADE_FAILED" + elif args[1:-1] == ["-m", "pip", "install", "-r"]: + installing = "requirements" + assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS" + elif args[1:] == ["-m", "pip", "install", "-e", ".[extras]"]: + installing = "pyproject" + assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT" + + create_venv.run_process = run_process + + create_venv.main(["--install"]) + assert pip_upgraded + assert installing == install_type diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index a5570b28e5da..3dea7e4a6185 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -33,33 +33,34 @@ export enum CommandSource { } export namespace Commands { - export const Set_Interpreter = 'python.setInterpreter'; - export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; - export const Exec_In_Terminal = 'python.execInTerminal'; - export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; - export const Debug_In_Terminal = 'python.debugInTerminal'; - export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; - export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; - export const Tests_Configure = 'python.configureTests'; - export const Sort_Imports = 'python.sortImports'; - export const ViewOutput = 'python.viewOutput'; - export const Start_REPL = 'python.startREPL'; + export const ClearStorage = 'python.clearCacheAndReload'; + export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; + export const Create_Environment = 'python.createEnvironment'; export const Create_Terminal = 'python.createTerminal'; - export const Set_Linter = 'python.setLinter'; + export const Debug_In_Terminal = 'python.debugInTerminal'; export const Enable_Linter = 'python.enableLinting'; - export const Run_Linter = 'python.runLinting'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; - export const PickLocalProcess = 'python.pickLocalProcess'; + export const Exec_In_Terminal = 'python.execInTerminal'; + export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; + export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; - export const ClearStorage = 'python.clearCacheAndReload'; - export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; + export const InstallPython = 'python.installPython'; + export const InstallPythonOnLinux = 'python.installPythonOnLinux'; + export const InstallPythonOnMac = 'python.installPythonOnMac'; export const LaunchTensorBoard = 'python.launchTensorBoard'; + export const PickLocalProcess = 'python.pickLocalProcess'; export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; - export const InstallPython = 'python.installPython'; - export const InstallPythonOnMac = 'python.installPythonOnMac'; - export const InstallPythonOnLinux = 'python.installPythonOnLinux'; + export const Run_Linter = 'python.runLinting'; + export const Set_Interpreter = 'python.setInterpreter'; + export const Set_Linter = 'python.setLinter'; + export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; + export const Sort_Imports = 'python.sortImports'; + export const Start_REPL = 'python.startREPL'; + export const Tests_Configure = 'python.configureTests'; export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; + export const ViewOutput = 'python.viewOutput'; } // Look at https://microsoft.github.io/vscode-codicons/dist/codicon.html for other Octicon icon ids diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index e0749cc18c67..d3f28097a5dc 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -158,3 +158,13 @@ export function linterScript(): string { const script = path.join(SCRIPTS_DIR, 'linter.py'); return script; } + +export function createVenvScript(): string { + const script = path.join(SCRIPTS_DIR, 'create_venv.py'); + return script; +} + +export function createCondaScript(): string { + const script = path.join(SCRIPTS_DIR, 'create_conda.py'); + return script; +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 2928f640d1be..412fe412f3d8 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -549,3 +549,74 @@ export namespace SwitchToDefaultLS { "The Microsoft Python Language Server has reached end of life. Your language server has been set to the default for Python in VS Code, Pylance.\n\nIf you'd like to change your language server, you can learn about how to do so [here](https://devblogs.microsoft.com/python/python-in-visual-studio-code-may-2021-release/#configuring-your-language-server).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); } + +export namespace CreateEnv { + export const statusTitle = localize('createEnv.statusTitle', 'Creating environment'); + export const statusStarting = localize('createEnv.statusStarting', 'Starting...'); + export const statusError = localize('createEnv.statusError', 'Error.'); + export const statusDone = localize('createEnv.statusDone', 'Done.'); + + export const hasVirtualEnv = localize('createEnv.hasVirtualEnv', 'Workspace folder contains a virtual environment'); + + export const noWorkspace = localize( + 'createEnv.noWorkspace', + 'Please open a directory when creating an environment using venv.', + ); + + export const pickWorkspaceTitle = localize( + 'createEnv.workspaceQuickPick.title', + 'Select a workspace to create environment', + ); + + export const providersQuickPickTitle = localize('createEnv.providersQuickPick.title', 'Select an environment type'); + + export namespace Venv { + export const creating = localize('createEnv.venv.creating', 'Creating venv...'); + export const created = localize('createEnv.venv.created', 'Environment created...'); + export const installingPackages = localize('createEnv.venv.installingPackages', 'Installing packages...'); + export const waitingForPython = localize('createEnv.venv.waitingForPython', 'Waiting on Python selection...'); + export const waitingForWorkspace = localize( + 'createEnv.venv.waitingForWorkspace', + 'Waiting on workspace selection...', + ); + export const selectPythonQuickPickTitle = localize( + 'createEnv.venv.basePython.title', + 'Select a python to use for environment creation', + ); + export const providerDescription = localize( + 'createEnv.venv.description', + 'Creates a `.venv` virtual environment in the current workspace', + ); + } + + export namespace Conda { + export const condaMissing = localize( + 'createEnv.conda.missing', + 'Please install `conda` to create conda environments.', + ); + export const created = localize('createEnv.conda.created', 'Environment created...'); + export const installingPackages = localize('createEnv.conda.installingPackages', 'Installing packages...'); + export const errorCreatingEnvironment = localize( + 'createEnv.conda.errorCreatingEnvironment', + 'Error while creating conda environment.', + ); + export const waitingForWorkspace = localize( + 'createEnv.conda.waitingForWorkspace', + 'Waiting on workspace selection...', + ); + export const waitingForPython = localize( + 'createEnv.conda.waitingForPython', + 'Waiting on Python version selection...', + ); + export const selectPythonQuickPickTitle = localize( + 'createEnv.conda.pythonSelection.title', + 'Please select the version of Python to install in the environment', + ); + export const searching = localize('createEnv.conda.searching', 'Searching for conda (base)...'); + export const creating = localize('createEnv.venv.creating', 'Running conda create...'); + export const providerDescription = localize( + 'createEnv.conda.description', + 'Creates a `.conda` Conda environment in the current workspace', + ); + } +} diff --git a/src/client/common/vscodeApis/commandApis.ts b/src/client/common/vscodeApis/commandApis.ts new file mode 100644 index 000000000000..580760e106e1 --- /dev/null +++ b/src/client/common/vscodeApis/commandApis.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { commands, Disposable } from 'vscode'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return commands.registerCommand(command, callback, thisArg); +} + +export function executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); +} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts new file mode 100644 index 000000000000..7def82abc752 --- /dev/null +++ b/src/client/common/vscodeApis/windowApis.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + MessageItem, + MessageOptions, + Progress, + ProgressOptions, + QuickPickItem, + QuickPickOptions, + window, +} from 'vscode'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function showQuickPick( + items: readonly T[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, +): Thenable { + return window.showQuickPick(items, options, token); +} + +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showErrorMessage(message: string, ...items: any[]): Thenable { + return window.showErrorMessage(message, ...items); +} + +export function withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, +): Thenable { + return window.withProgress(options, task); +} diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts new file mode 100644 index 000000000000..9e9af9e6f699 --- /dev/null +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { workspace, WorkspaceFolder } from 'vscode'; + +export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined { + return workspace.workspaceFolders; +} + +export function getWorkspaceFolderPaths(): string[] { + return workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? []; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 312e99a38683..6069583489ef 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -35,7 +35,7 @@ import { IApplicationShell, IWorkspaceService } from './common/application/types import { IDisposableRegistry, IExperimentService, IExtensionContext } from './common/types'; import { createDeferred } from './common/utils/async'; import { Common } from './common/utils/localize'; -import { activateComponents } from './extensionActivation'; +import { activateComponents, activateFeatures } from './extensionActivation'; import { initializeStandard, initializeComponents, initializeGlobals } from './extensionInit'; import { IServiceContainer } from './ioc/types'; import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; @@ -128,6 +128,8 @@ async function activateUnsafe( // Then we finish activating. const componentsActivated = await activateComponents(ext, components); + activateFeatures(ext, components); + const nonBlocking = componentsActivated.map((r) => r.fullyReady); const activationPromise = (async () => { await Promise.all(nonBlocking); diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 54346088d252..3d2e026d7da4 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -61,6 +61,8 @@ import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHan import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; +import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; +import { IInterpreterQuickPick } from './interpreter/configuration/types'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -95,6 +97,13 @@ export async function activateComponents( return Promise.all([legacyActivationResult, ...promises]); } +export function activateFeatures(ext: ExtensionState, components: Components): void { + const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( + IInterpreterQuickPick, + ); + registerCreateEnvironmentFeatures(ext.disposables, components.pythonEnvs, interpreterQuickPick); +} + /// ////////////////////////// // old activation code diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index a8ad85b05540..44a69019601c 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -24,7 +24,7 @@ import { getInterpreterPathFromDir, getPythonVersionFromPath, } from '../../../common/commonUtils'; -import { arePathsSame, getFileInfo, getWorkspaceFolders, isParentPath } from '../../../common/externalDependencies'; +import { arePathsSame, getFileInfo, isParentPath } from '../../../common/externalDependencies'; import { AnacondaCompanyName, Conda, isCondaEnvironment } from '../../../common/environmentManagers/conda'; import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv'; import { Architecture, getOSType, OSType } from '../../../../common/utils/platform'; @@ -34,6 +34,7 @@ import { BasicEnvInfo } from '../../locator'; import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; +import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -91,7 +92,7 @@ async function getEnvType(env: PythonEnvInfo) { } function getSearchLocation(env: PythonEnvInfo): Uri | undefined { - const folders = getWorkspaceFolders(); + const folders = getWorkspaceFolderPaths(); const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f)); if (isRootedEnv) { // For environments inside roots, we need to set search location so they can be queried accordingly. diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 6ee5f1e5f6a1..d1ec026e6b4c 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -84,10 +84,6 @@ export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } -export function getWorkspaceFolders(): string[] { - return vscode.workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? []; -} - export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats): Promise { stats = stats ?? (await fsapi.lstat(absPath)); if (stats.isSymbolicLink()) { diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts new file mode 100644 index 000000000000..f8fdeebbcf20 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { Commands } from '../../../common/constants'; +import { Common } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; + +export async function showErrorMessageWithLogs(message: string): Promise { + const result = await showErrorMessage(message, Common.openOutputPanel); + if (result === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } +} diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts new file mode 100644 index 000000000000..810d51856bb8 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { QuickPickItem, WorkspaceFolder } from 'vscode'; +import { showErrorMessage, showQuickPick } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; +import { CreateEnv } from '../../../common/utils/localize'; + +function hasVirtualEnv(workspace: WorkspaceFolder): Promise { + return Promise.race([ + fsapi.pathExists(path.join(workspace.uri.fsPath, '.venv')), + fsapi.pathExists(path.join(workspace.uri.fsPath, '.conda')), + ]); +} + +async function getWorkspacesForQuickPick(workspaces: readonly WorkspaceFolder[]): Promise { + const items: QuickPickItem[] = []; + for (const workspace of workspaces) { + items.push({ + label: workspace.name, + detail: workspace.uri.fsPath, + description: (await hasVirtualEnv(workspace)) ? CreateEnv.hasVirtualEnv : undefined, + }); + } + + return items; +} + +export interface PickWorkspaceFolderOptions { + allowMultiSelect?: boolean; +} + +export async function pickWorkspaceFolder( + options?: PickWorkspaceFolderOptions, +): Promise { + const workspaces = getWorkspaceFolders(); + + if (!workspaces || workspaces.length === 0) { + showErrorMessage(CreateEnv.noWorkspace); + return undefined; + } + + if (workspaces.length === 1) { + return workspaces[0]; + } + + // This is multi-root scenario. + const selected = await showQuickPick(getWorkspacesForQuickPick(workspaces), { + title: CreateEnv.pickWorkspaceTitle, + ignoreFocusOut: true, + canPickMany: options?.allowMultiSelect, + }); + + if (selected) { + if (options?.allowMultiSelect) { + const details = ((selected as unknown) as QuickPickItem[]) + .map((s: QuickPickItem) => s.detail) + .filter((s) => s !== undefined); + return workspaces.filter((w) => details.includes(w.uri.fsPath)); + } + return workspaces.filter((w) => w.uri.fsPath === selected.detail)[0]; + } + + return undefined; +} diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts new file mode 100644 index 000000000000..2546a858ced9 --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable } from 'vscode'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { registerCommand } from '../../common/vscodeApis/commandApis'; +import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { IDiscoveryAPI } from '../base/locator'; +import { handleCreateEnvironmentCommand } from './createEnvQuickPick'; +import { condaCreationProvider } from './provider/condaCreationProvider'; +import { VenvCreationProvider } from './provider/venvCreationProvider'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider } from './types'; + +class CreateEnvironmentProviders { + private _createEnvProviders: CreateEnvironmentProvider[] = []; + + constructor() { + this._createEnvProviders = []; + } + + public add(provider: CreateEnvironmentProvider) { + this._createEnvProviders.push(provider); + } + + public remove(provider: CreateEnvironmentProvider) { + this._createEnvProviders = this._createEnvProviders.filter((p) => p !== provider); + } + + public getAll(): readonly CreateEnvironmentProvider[] { + return this._createEnvProviders; + } +} + +const _createEnvironmentProviders: CreateEnvironmentProviders = new CreateEnvironmentProviders(); + +export function registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable { + _createEnvironmentProviders.add(provider); + return new Disposable(() => { + _createEnvironmentProviders.remove(provider); + }); +} + +export function registerCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + discoveryApi: IDiscoveryAPI, + interpreterQuickPick: IInterpreterQuickPick, +): void { + disposables.push( + registerCommand( + Commands.Create_Environment, + (options?: CreateEnvironmentOptions): Promise => { + const providers = _createEnvironmentProviders.getAll(); + return handleCreateEnvironmentCommand(providers, options); + }, + ), + ); + disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(discoveryApi, interpreterQuickPick))); + disposables.push(registerCreateEnvironmentProvider(condaCreationProvider())); +} diff --git a/src/client/pythonEnvironments/creation/createEnvQuickPick.ts b/src/client/pythonEnvironments/creation/createEnvQuickPick.ts new file mode 100644 index 000000000000..de71aa84cd06 --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvQuickPick.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { QuickPickItem } from 'vscode'; +import { CreateEnv } from '../../common/utils/localize'; +import { showQuickPick } from '../../common/vscodeApis/windowApis'; +import { traceError } from '../../logging'; +import { createEnvironment } from './createEnvironment'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider } from './types'; + +interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { + id: string; +} + +async function showCreateEnvironmentQuickPick( + providers: readonly CreateEnvironmentProvider[], +): Promise { + const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ + label: p.name, + description: p.description, + id: p.id, + })); + const selected = await showQuickPick(items, { + title: CreateEnv.providersQuickPickTitle, + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selected) { + const selections = providers.filter((p) => p.id === selected.id); + if (selections.length > 0) { + return selections[0]; + } + } + return undefined; +} + +export async function handleCreateEnvironmentCommand( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions, +): Promise { + if (providers.length === 1) { + return createEnvironment(providers[0], options); + } + if (providers.length > 1) { + const provider = await showCreateEnvironmentQuickPick(providers); + if (provider) { + return createEnvironment(provider, options); + } + } else { + traceError('No Environment Creation providers were registered.'); + } + return undefined; +} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts new file mode 100644 index 000000000000..dc6dc9461cfd --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { CancellationToken, ProgressLocation } from 'vscode'; +import { withProgress } from '../../common/vscodeApis/windowApis'; +import { traceError } from '../../logging'; +import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from './types'; +import { CreateEnv } from '../../common/utils/localize'; + +export async function createEnvironment( + provider: CreateEnvironmentProvider, + options: CreateEnvironmentOptions = { + ignoreSourceControl: true, + installPackages: true, + }, +): Promise { + return withProgress( + { + location: ProgressLocation.Notification, + title: CreateEnv.statusTitle, + cancellable: true, + }, + async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + let hasError = false; + progress.report({ + message: CreateEnv.statusStarting, + }); + try { + const result = await provider.createEnvironment(options, progress, token); + return result; + } catch (ex) { + traceError(ex); + hasError = true; + progress.report({ + message: CreateEnv.statusError, + }); + throw ex; + } finally { + if (!hasError) { + progress.report({ + message: CreateEnv.statusDone, + }); + } + } + }, + ); +} diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts new file mode 100644 index 000000000000..81ef43334449 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { traceError, traceLog } from '../../../logging'; +import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { execObservable } from '../../../common/process/rawProcessApis'; +import { createDeferred } from '../../../common/utils/async'; +import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { createCondaScript } from '../../../common/process/internal/scripts'; +import { CreateEnv } from '../../../common/utils/localize'; +import { getConda, pickPythonVersion } from './condaUtils'; +import { showErrorMessageWithLogs } from '../common/commonUtils'; + +export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; +export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; + +function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + + const command: string[] = [createCondaScript()]; + + if (addGitIgnore) { + command.push('--git-ignore'); + } + + if (installPackages) { + command.push('--install'); + } + + if (version) { + command.push('--python'); + command.push(version); + } + + return command; +} + +async function createCondaEnv( + workspace: WorkspaceFolder, + command: string, + args: string[], + progress?: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + const deferred = createDeferred(); + let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; + if (getOSType() === OSType.Windows) { + // On windows `conda.bat` is used, which adds the following bin directories to PATH + // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are + // instead using the `python.exe` that ships with conda to run a python script that + // handles conda env creation and package installation. + // See conda issue: https://github.com/conda/conda/issues/11399 + const root = path.dirname(command); + const libPath1 = path.join(root, 'Library', 'bin'); + const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); + const libPath3 = path.join(root, 'Library', 'usr', 'bin'); + const libPath4 = path.join(root, 'bin'); + const libPath5 = path.join(root, 'Scripts'); + const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); + pathEnv = `${libPath}${path.delimiter}${pathEnv}`; + } + traceLog('Running Conda Env creation script: ', [command, ...args]); + const { out, dispose } = execObservable(command, args, { + mergeStdOutErr: true, + token, + cwd: workspace.uri.fsPath, + env: { + PATH: pathEnv, + }, + }); + + let condaEnvPath: string | undefined; + out.subscribe( + (value) => { + const output = value.out.splitLines().join('\r\n'); + traceLog(output); + if (output.includes(CONDA_ENV_CREATED_MARKER)) { + progress?.report({ + message: CreateEnv.Conda.created, + }); + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER))[0]; + condaEnvPath = envPath.substring(CONDA_ENV_CREATED_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + condaEnvPath = undefined; + } + } else if (output.includes(CONDA_INSTALLING_YML)) { + progress?.report({ + message: CreateEnv.Conda.installingPackages, + }); + } + }, + async (error) => { + traceError('Error while running conda env creation script: ', error); + deferred.reject(error); + await showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + }, + () => { + dispose(); + if (!deferred.rejected) { + deferred.resolve(condaEnvPath); + } + }, + ); + return deferred.promise; +} + +function getExecutableCommand(condaPath: string): string { + if (getOSType() === OSType.Windows) { + // Both Miniconda3 and Anaconda3 have the following structure: + // Miniconda3 (or Anaconda3) + // |- condabin + // | |- conda.bat <--- this actually points to python.exe below, + // | after adding few paths to PATH. + // |- Scripts + // | |- conda.exe <--- this is the path we get as condaPath, + // | which is really a stub for `python.exe -m conda`. + // |- python.exe <--- this is the python that we want. + return path.join(path.dirname(path.dirname(condaPath)), 'python.exe'); + } + // On non-windows machines: + // miniconda (or miniforge or anaconda3) + // |- bin + // |- conda <--- this is the path we get as condaPath. + // |- python <--- this is the python that we want. + return path.join(path.dirname(condaPath), 'python'); +} + +async function createEnvironment( + options?: CreateEnvironmentOptions, + progress?: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + progress?.report({ + message: CreateEnv.Conda.searching, + }); + const conda = await getConda(); + if (!conda) { + return undefined; + } + + progress?.report({ + message: CreateEnv.Conda.waitingForWorkspace, + }); + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; + if (!workspace) { + traceError('Workspace was not selected or found for creating virtual env.'); + return undefined; + } + + progress?.report({ + message: CreateEnv.Conda.waitingForPython, + }); + const version = await pickPythonVersion(); + if (!version) { + traceError('Conda environments for use with python extension require Python.'); + return undefined; + } + + progress?.report({ + message: CreateEnv.Conda.creating, + }); + const args = generateCommandArgs(version, options); + return createCondaEnv(workspace, getExecutableCommand(conda), args, progress, token); +} + +export function condaCreationProvider(): CreateEnvironmentProvider { + return { + createEnvironment, + name: 'Conda', + + description: CreateEnv.Conda.providerDescription, + + id: `${PVSC_EXTENSION_ID}:conda`, + }; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts new file mode 100644 index 000000000000..fd35c82267ac --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { QuickPickItem, Uri } from 'vscode'; +import { Common } from '../../../browser/localize'; +import { CreateEnv } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; +import { showErrorMessage, showQuickPick } from '../../../common/vscodeApis/windowApis'; +import { Conda } from '../../common/environmentManagers/conda'; + +export async function getConda(): Promise { + const conda = await Conda.getConda(); + + if (!conda) { + const response = await showErrorMessage(CreateEnv.Conda.condaMissing, Common.learnMore); + if (response === Common.learnMore) { + await executeCommand('vscode.open', Uri.parse('https://docs.anaconda.com/anaconda/install/')); + } + return undefined; + } + return conda.command; +} + +export async function pickPythonVersion(): Promise { + const items: QuickPickItem[] = ['3.7', '3.8', '3.9', '3.10'].map((v) => ({ + label: `Python`, + description: v, + })); + const version = await showQuickPick(items, { + title: CreateEnv.Conda.selectPythonQuickPickTitle, + }); + return version?.description; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts new file mode 100644 index 000000000000..f6ad31374eb3 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, WorkspaceFolder } from 'vscode'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { createVenvScript } from '../../../common/process/internal/scripts'; +import { execObservable } from '../../../common/process/rawProcessApis'; +import { createDeferred } from '../../../common/utils/async'; +import { CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceLog } from '../../../logging'; +import { PythonEnvKind } from '../../base/info'; +import { IDiscoveryAPI } from '../../base/locator'; +import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; +import { EnvironmentType, PythonEnvironment } from '../../info'; + +export const VENV_CREATED_MARKER = 'CREATED_VENV:'; +export const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +export const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +export const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +export const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +export const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +export const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; + +function generateCommandArgs(options?: CreateEnvironmentOptions): string[] { + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + + const command: string[] = [createVenvScript()]; + + if (addGitIgnore) { + command.push('--git-ignore'); + } + + if (installPackages) { + command.push('--install'); + } + + return command; +} + +async function createVenv( + workspace: WorkspaceFolder, + command: string, + args: string[], + progress?: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + progress?.report({ + message: CreateEnv.Venv.creating, + }); + const deferred = createDeferred(); + traceLog('Running Env creation script: ', [command, ...args]); + const { out, dispose } = execObservable(command, args, { + mergeStdOutErr: true, + token, + cwd: workspace.uri.fsPath, + }); + + let venvPath: string | undefined; + out.subscribe( + (value) => { + const output = value.out.split(/\r?\n/g).join('\r\n'); + traceLog(output); + if (output.includes(VENV_CREATED_MARKER)) { + progress?.report({ + message: CreateEnv.Venv.created, + }); + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(VENV_CREATED_MARKER))[0]; + venvPath = envPath.substring(VENV_CREATED_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + venvPath = undefined; + } + } else if (output.includes(INSTALLING_REQUIREMENTS) || output.includes(INSTALLING_PYPROJECT)) { + progress?.report({ + message: CreateEnv.Venv.installingPackages, + }); + } + }, + (error) => { + traceError('Error while running venv creation script: ', error); + deferred.reject(error); + }, + () => { + dispose(); + if (!deferred.rejected) { + deferred.resolve(venvPath); + } + }, + ); + return deferred.promise; +} + +export class VenvCreationProvider implements CreateEnvironmentProvider { + constructor( + private readonly discoveryApi: IDiscoveryAPI, + private readonly interpreterQuickPick: IInterpreterQuickPick, + ) {} + + public async createEnvironment( + options?: CreateEnvironmentOptions, + progress?: CreateEnvironmentProgress, + token?: CancellationToken, + ): Promise { + progress?.report({ + message: CreateEnv.Venv.waitingForWorkspace, + }); + + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; + if (workspace === undefined) { + traceError('Workspace was not selected or found for creating virtual environment.'); + return undefined; + } + + progress?.report({ + message: CreateEnv.Venv.waitingForPython, + }); + const interpreters = this.discoveryApi.getEnvs({ + kinds: [PythonEnvKind.MicrosoftStore, PythonEnvKind.OtherGlobal], + }); + + const args = generateCommandArgs(options); + if (interpreters.length === 1) { + return createVenv(workspace, interpreters[0].executable.filename, args, progress, token); + } + + const interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [EnvironmentType.System, EnvironmentType.MicrosoftStore, EnvironmentType.Global].includes(i.envType), + ); + + if (interpreter) { + return createVenv(workspace, interpreter, args, progress, token); + } + + traceError('Virtual env creation requires an interpreter.'); + return undefined; + } + + name = 'Venv'; + + description: string = CreateEnv.Venv.providerDescription; + + id = `${PVSC_EXTENSION_ID}:venv`; +} diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts new file mode 100644 index 000000000000..9e9a31799d09 --- /dev/null +++ b/src/client/pythonEnvironments/creation/types.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { CancellationToken, Progress } from 'vscode'; + +export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} + +export interface CreateEnvironmentOptions { + installPackages?: boolean; + ignoreSourceControl?: boolean; +} + +export interface CreateEnvironmentProvider { + createEnvironment( + options?: CreateEnvironmentOptions, + progress?: CreateEnvironmentProgress, + token?: CancellationToken, + ): Promise; + name: string; + description: string; + id: string; +} diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts index f0c1a7d022f0..4a480cfd6e44 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -38,6 +38,7 @@ import { createBasicEnv, getEnvs, getEnvsWithUpdates, SimpleLocator } from '../. import { getOSType, OSType } from '../../../../common'; import { CondaInfo } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; import { createDeferred } from '../../../../../client/common/utils/async'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; suite('Python envs locator - Environments Resolver', () => { let envInfoService: IEnvironmentInfoService; @@ -115,7 +116,7 @@ suite('Python envs locator - Environments Resolver', () => { }); }), ); - sinon.stub(externalDependencies, 'getWorkspaceFolders').returns([testVirtualHomeDir]); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); }); teardown(() => { @@ -349,7 +350,7 @@ suite('Python envs locator - Environments Resolver', () => { }); }), ); - sinon.stub(externalDependencies, 'getWorkspaceFolders').returns([testVirtualHomeDir]); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); }); teardown(() => { diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts index 322a4cb8cc91..2310f6dc942f 100644 --- a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -26,12 +26,13 @@ import { CondaInfo, } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; import { resolveBasicEnv } from '../../../../../client/pythonEnvironments/base/locators/composite/resolverUtils'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; suite('Resolver Utils', () => { let getWorkspaceFolders: sinon.SinonStub; setup(() => { sinon.stub(externalDependencies, 'getPythonSetting').withArgs('condaPath').returns('conda'); - getWorkspaceFolders = sinon.stub(externalDependencies, 'getWorkspaceFolders'); + getWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); getWorkspaceFolders.returns([]); }); diff --git a/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts new file mode 100644 index 000000000000..03ec08ecd83b --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +// import * as typemoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { pickWorkspaceFolder } from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; + +suite('Create environment workspace selection tests', () => { + let showQuickPickStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No workspaces (undefined)', async () => { + getWorkspaceFoldersStub.returns(undefined); + assert.isUndefined(await pickWorkspaceFolder()); + assert.isTrue(showErrorMessageStub.calledOnce); + }); + + test('No workspaces (empty array)', async () => { + getWorkspaceFoldersStub.returns([]); + assert.isUndefined(await pickWorkspaceFolder()); + assert.isTrue(showErrorMessageStub.calledOnce); + }); + + test('User did not select workspace', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file('some_folder'), + name: 'some_folder', + index: 0, + }, + { + uri: Uri.file('some_folder2'), + name: 'some_folder2', + index: 1, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickStub.returns(undefined); + assert.isUndefined(await pickWorkspaceFolder()); + }); + + test('single workspace scenario', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickStub.returns({ + label: workspaces[0].name, + detail: workspaces[0].uri.fsPath, + description: undefined, + }); + + const workspace = await pickWorkspaceFolder(); + assert.deepEqual(workspace, workspaces[0]); + assert(showQuickPickStub.notCalled); + }); + + test('Multi-workspace scenario with single workspace selected', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace2')), + name: 'workspace2', + index: 1, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace3')), + name: 'workspace3', + index: 2, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace4')), + name: 'workspace4', + index: 3, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace5')), + name: 'workspace5', + index: 4, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickStub.returns({ + label: workspaces[1].name, + detail: workspaces[1].uri.fsPath, + description: undefined, + }); + + const workspace = await pickWorkspaceFolder(); + assert.deepEqual(workspace, workspaces[1]); + assert(showQuickPickStub.calledOnce); + }); + + test('Multi-workspace scenario with multiple workspaces selected', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace2')), + name: 'workspace2', + index: 1, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace3')), + name: 'workspace3', + index: 2, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace4')), + name: 'workspace4', + index: 3, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace5')), + name: 'workspace5', + index: 4, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickStub.returns([ + { + label: workspaces[1].name, + detail: workspaces[1].uri.fsPath, + description: undefined, + }, + { + label: workspaces[3].name, + detail: workspaces[3].uri.fsPath, + description: undefined, + }, + ]); + + const workspace = await pickWorkspaceFolder({ allowMultiSelect: true }); + assert.deepEqual(workspace, [workspaces[1], workspaces[3]]); + assert(showQuickPickStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts new file mode 100644 index 000000000000..165dff8c6b2b --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import * as createEnv from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvQuickPick'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; + +suite('Create Environment Command Handler Tests', () => { + let showQuickPickStub: sinon.SinonStub; + let createEnvironmentStub: sinon.SinonStub; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + createEnvironmentStub = sinon.stub(createEnv, 'createEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No providers registered', async () => { + await handleCreateEnvironmentCommand([]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(createEnvironmentStub.notCalled); + }); + + test('Single environment creation provider registered', async () => { + const provider = typemoq.Mock.ofType(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(showQuickPickStub.notCalled); + createEnvironmentStub.calledOnceWithExactly(provider.object, undefined); + }); + + test('Multiple environment creation providers registered', async () => { + const provider1 = typemoq.Mock.ofType(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + + const provider2 = typemoq.Mock.ofType(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + + showQuickPickStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + createEnvironmentStub.calledOnceWithExactly(provider2.object, undefined); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts new file mode 100644 index 000000000000..f9c12d1f6eb0 --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { ProgressLocation, ProgressOptions } from 'vscode'; +import { CreateEnv } from '../../../client/common/utils/localize'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { createEnvironment } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { + CreateEnvironmentProgress, + CreateEnvironmentProvider, +} from '../../../client/pythonEnvironments/creation/types'; + +chaiUse(chaiAsPromised); + +suite('Create Environments Tests', () => { + let withProgressStub: sinon.SinonStub; + let progressMock: typemoq.IMock; + + setup(() => { + progressMock = typemoq.Mock.ofType(); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + withProgressStub.callsFake(async (options: ProgressOptions, task) => { + assert.deepEqual(options, { + location: ProgressLocation.Notification, + title: CreateEnv.statusTitle, + cancellable: true, + }); + + await task(progressMock.object, undefined); + }); + }); + + teardown(() => { + progressMock.reset(); + sinon.restore(); + }); + + test('Successful environment creation', async () => { + const provider = typemoq.Mock.ofType(); + provider + .setup((p) => p.createEnvironment(typemoq.It.isAny(), progressMock.object, undefined)) + .returns(() => Promise.resolve(undefined)); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusDone })).verifiable(typemoq.Times.once()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusError })).verifiable(typemoq.Times.never()); + await createEnvironment(provider.object); + + progressMock.verifyAll(); + provider.verifyAll(); + }); + + test('Environment creation error', async () => { + const provider = typemoq.Mock.ofType(); + provider + .setup((p) => p.createEnvironment(typemoq.It.isAny(), progressMock.object, undefined)) + .returns(() => Promise.reject()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusDone })).verifiable(typemoq.Times.never()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusError })).verifiable(typemoq.Times.once()); + + await assert.isRejected(createEnvironment(provider.object)); + + progressMock.verifyAll(); + provider.verifyAll(); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts new file mode 100644 index 000000000000..fffcd2511a15 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { assert, use as chaiUse } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { CreateEnvironmentProvider } from '../../../../client/pythonEnvironments/creation/types'; +import { + condaCreationProvider, + CONDA_ENV_CREATED_MARKER, +} from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; +import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as condaUtils from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { Output } from '../../../../client/common/process/types'; +import { createDeferred } from '../../../../client/common/utils/async'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; + +chaiUse(chaiAsPromised); + +suite('Conda Creation provider tests', () => { + let condaProvider: CreateEnvironmentProvider; + let getCondaStub: sinon.SinonStub; + let pickPythonVersionStub: sinon.SinonStub; + let pickWorkspaceFolderStub: sinon.SinonStub; + let execObservableStub: sinon.SinonStub; + + let showErrorMessageWithLogsStub: sinon.SinonStub; + + setup(() => { + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + getCondaStub = sinon.stub(condaUtils, 'getConda'); + pickPythonVersionStub = sinon.stub(condaUtils, 'pickPythonVersion'); + execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + condaProvider = condaCreationProvider(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No conda installed', async () => { + getCondaStub.resolves(undefined); + + assert.isUndefined(await condaProvider.createEnvironment()); + }); + + test('No workspace selected', async () => { + getCondaStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves(undefined); + + assert.isUndefined(await condaProvider.createEnvironment()); + }); + + test('No python version picked selected', async () => { + getCondaStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves(undefined); + + assert.isUndefined(await condaProvider.createEnvironment()); + }); + + test('Create conda environment', async () => { + getCondaStub.resolves('/usr/bin/conda/conda_bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + assert.strictEqual(await promise, 'new_environment'); + }); + + test('Create conda environment failed', async () => { + getCondaStub.resolves('/usr/bin/conda/conda_bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _error: undefined | ((error: unknown) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + _next?: (value: Output) => void, + error?: (error: unknown) => void, + complete?: () => void, + ) => { + _error = error; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_error); + _error!('bad arguments'); + _complete!(); + await assert.isRejected(promise); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts new file mode 100644 index 000000000000..e30c86e78a4f --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { CreateEnvironmentProvider } from '../../../../client/pythonEnvironments/creation/types'; +import { + VenvCreationProvider, + VENV_CREATED_MARKER, +} from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; +import { IDiscoveryAPI } from '../../../../client/pythonEnvironments/base/locator'; +import { IInterpreterQuickPick } from '../../../../client/interpreter/configuration/types'; +import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { PythonEnvKind, PythonEnvSource } from '../../../../client/pythonEnvironments/base/info'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { Output } from '../../../../client/common/process/types'; + +chaiUse(chaiAsPromised); + +const python37 = { + name: 'Python 3.7', + kind: PythonEnvKind.System, + location: '/usr/bin/python3.7', + source: [PythonEnvSource.PathEnvVar], + executable: { + filename: '/usr/bin/python3.7', + ctime: 0, + mtime: 0, + sysPrefix: '', + }, + version: { + major: 3, + minor: 7, + micro: 7, + }, + arch: Architecture.x64, + distro: { + org: 'python', + }, +}; +const python38 = { + name: 'Python 3.8', + kind: PythonEnvKind.System, + location: '/usr/bin/python3.8', + source: [PythonEnvSource.PathEnvVar], + executable: { + filename: '/usr/bin/python3.8', + ctime: 0, + mtime: 0, + sysPrefix: '', + }, + version: { + major: 3, + minor: 8, + micro: 8, + }, + arch: Architecture.x64, + distro: { + org: 'python', + }, +}; + +suite('venv Creation provider tests', () => { + let venvProvider: CreateEnvironmentProvider; + let pickWorkspaceFolderStub: sinon.SinonStub; + let discoveryApi: typemoq.IMock; + let interpreterQuickPick: typemoq.IMock; + let execObservableStub: sinon.SinonStub; + + setup(() => { + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + discoveryApi = typemoq.Mock.ofType(); + interpreterQuickPick = typemoq.Mock.ofType(); + venvProvider = new VenvCreationProvider(discoveryApi.object, interpreterQuickPick.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No workspace selected', async () => { + pickWorkspaceFolderStub.resolves(undefined); + + assert.isUndefined(await venvProvider.createEnvironment()); + assert.isTrue(pickWorkspaceFolderStub.calledOnce); + }); + + test('No Python selected', async () => { + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + + // Return multiple envs here to force user selection. + discoveryApi + .setup((d) => d.getEnvs(typemoq.It.isAny())) + .returns(() => [python37, python38]) + .verifiable(typemoq.Times.once()); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + assert.isUndefined(await venvProvider.createEnvironment()); + discoveryApi.verifyAll(); + interpreterQuickPick.verifyAll(); + }); + + test('Create venv with single global python', async () => { + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + + // Return single env here to skip user selection. + discoveryApi + .setup((d) => d.getEnvs(typemoq.It.isAny())) + .returns(() => [python38]) + .verifiable(typemoq.Times.once()); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.never()); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + assert.strictEqual(await promise, 'new_environment'); + discoveryApi.verifyAll(); + interpreterQuickPick.verifyAll(); + }); + + test('Create venv with multiple global python', async () => { + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + + // Return single env here to skip user selection. + discoveryApi + .setup((d) => d.getEnvs(typemoq.It.isAny())) + .returns(() => [python37, python38]) + .verifiable(typemoq.Times.once()); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(python38.executable.filename)) + .verifiable(typemoq.Times.once()); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + assert.strictEqual(await promise, 'new_environment'); + discoveryApi.verifyAll(); + interpreterQuickPick.verifyAll(); + }); + + test('Create venv failed', async () => { + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + + // Return single env here to skip user selection. + discoveryApi + .setup((d) => d.getEnvs(typemoq.It.isAny())) + .returns(() => [python38]) + .verifiable(typemoq.Times.once()); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.never()); + + const deferred = createDeferred(); + let _error: undefined | ((error: unknown) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + _next?: (value: Output) => void, + error?: (error: unknown) => void, + complete?: () => void, + ) => { + _error = error; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_error); + _error!('bad arguments'); + _complete!(); + await assert.isRejected(promise); + }); +}); From 2eb34e57020fd6e5c4389a5e4047927a30c4a333 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 23 Sep 2022 18:33:35 -0700 Subject: [PATCH 42/59] Watch for new conda environments (#19877) --- .../base/locators/lowLevel/condaLocator.ts | 17 ++- .../locators/lowLevel/fsWatchingLocator.ts | 82 ++++++++----- .../common/environmentManagers/conda.ts | 12 ++ src/client/pythonEnvironments/legacyIOC.ts | 23 +--- .../lowLevel/condaLocator.testvirtualenvs.ts | 114 ++++++++++++++++++ 5 files changed, 194 insertions(+), 54 deletions(-) create mode 100644 src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts index 3c0b691ec925..ed7848922893 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts @@ -2,13 +2,22 @@ // Licensed under the MIT License. import '../../../../common/extensions'; import { PythonEnvKind } from '../../info'; -import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator'; -import { Conda } from '../../../common/environmentManagers/conda'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { Conda, getCondaEnvironmentsTxt } from '../../../common/environmentManagers/conda'; import { traceError, traceVerbose } from '../../../../logging'; +import { FSWatchingLocator } from './fsWatchingLocator'; + +export class CondaEnvironmentLocator extends FSWatchingLocator { + public constructor() { + super( + () => getCondaEnvironmentsTxt(), + async () => PythonEnvKind.Conda, + { isFile: true }, + ); + } -export class CondaEnvironmentLocator extends Locator { // eslint-disable-next-line class-methods-use-this - public async *iterEnvs(): IPythonEnvsIterator { + public async *doIterEnvs(): IPythonEnvsIterator { const conda = await Conda.getConda(); if (conda === undefined) { traceVerbose(`Couldn't locate the conda binary.`); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts index bccc705aa818..6d2c93829906 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { Uri } from 'vscode'; -import { FileChangeType } from '../../../../common/platform/fileSystemWatcher'; +import { FileChangeType, watchLocationForPattern } from '../../../../common/platform/fileSystemWatcher'; import { sleep } from '../../../../common/utils/async'; import { traceError, traceVerbose } from '../../../../logging'; import { getEnvironmentDirFromPath } from '../../../common/commonUtils'; @@ -47,6 +47,33 @@ function checkDirWatchable(dirname: string): DirUnwatchableReason { return undefined; } +type LocationWatchOptions = { + /** + * Glob which represents basename of the executable or directory to watch. + */ + baseGlob?: string; + /** + * Time to wait before handling an environment-created event. + */ + delayOnCreated?: number; // milliseconds + /** + * Location affected by the event. If not provided, a default search location is used. + */ + searchLocation?: string; + /** + * The Python env structure to watch. + */ + envStructure?: PythonEnvStructure; +}; + +type FileWatchOptions = { + /** + * If the provided root is a file instead. In this case the file is directly watched instead for + * looking for python binaries inside a root. + */ + isFile: boolean; +}; + /** * The base for Python envs locators who watch the file system. * Most low-level locators should be using this. @@ -63,24 +90,7 @@ export abstract class FSWatchingLocator extends LazyResourceB * Returns the kind of environment specific to locator given the path to executable. */ private readonly getKind: (executable: string) => Promise, - private readonly opts: { - /** - * Glob which represents basename of the executable or directory to watch. - */ - baseGlob?: string; - /** - * Time to wait before handling an environment-created event. - */ - delayOnCreated?: number; // milliseconds - /** - * Location affected by the event. If not provided, a default search location is used. - */ - searchLocation?: string; - /** - * The Python env structure to watch. - */ - envStructure?: PythonEnvStructure; - } = {}, + private readonly creationOptions: LocationWatchOptions | FileWatchOptions = {}, private readonly watcherKind: FSWatcherKind = FSWatcherKind.Global, ) { super(); @@ -89,8 +99,8 @@ export abstract class FSWatchingLocator extends LazyResourceB protected async initWatchers(): Promise { // Enable all workspace watchers. - if (this.watcherKind === FSWatcherKind.Global) { - // Do not allow global watchers for now + if (this.watcherKind === FSWatcherKind.Global && !isWatchingAFile(this.creationOptions)) { + // Do not allow global location watchers for now. return; } @@ -102,6 +112,9 @@ export abstract class FSWatchingLocator extends LazyResourceB roots = [roots]; } const promises = roots.map(async (root) => { + if (isWatchingAFile(this.creationOptions)) { + return root; + } // Note that we only check the root dir. Any directories // that might be watched due to a glob are not checked. const unwatchable = await checkDirWatchable(root); @@ -116,12 +129,23 @@ export abstract class FSWatchingLocator extends LazyResourceB } private startWatchers(root: string): void { + const opts = this.creationOptions; + if (isWatchingAFile(opts)) { + traceVerbose('Start watching file for changes', root); + this.disposables.push( + watchLocationForPattern(path.dirname(root), path.basename(root), () => { + traceVerbose('Detected change in file: ', root, 'initiating a refresh'); + this.emitter.fire({}); + }), + ); + return; + } const callback = async (type: FileChangeType, executable: string) => { if (type === FileChangeType.Created) { - if (this.opts.delayOnCreated !== undefined) { + if (opts.delayOnCreated !== undefined) { // Note detecting kind of env depends on the file structure around the // executable, so we need to wait before attempting to detect it. - await sleep(this.opts.delayOnCreated); + await sleep(opts.delayOnCreated); } } // Fetching kind after deletion normally fails because the file structure around the @@ -135,20 +159,22 @@ export abstract class FSWatchingLocator extends LazyResourceB // |__ env // |__ bin or Scripts // |__ python <--- executable - const searchLocation = Uri.file( - this.opts.searchLocation ?? path.dirname(getEnvironmentDirFromPath(executable)), - ); + const searchLocation = Uri.file(opts.searchLocation ?? path.dirname(getEnvironmentDirFromPath(executable))); traceVerbose('Fired event ', JSON.stringify({ type, kind, searchLocation }), 'from locator'); this.emitter.fire({ type, kind, searchLocation }); }; const globs = resolvePythonExeGlobs( - this.opts.baseGlob, + opts.baseGlob, // The structure determines which globs are returned. - this.opts.envStructure, + opts.envStructure, ); traceVerbose('Start watching root', root, 'for globs', JSON.stringify(globs)); const watchers = globs.map((g) => watchLocationForPythonBinaries(root, callback, g)); this.disposables.push(...watchers); } } + +function isWatchingAFile(options: LocationWatchOptions | FileWatchOptions): options is FileWatchOptions { + return 'isFile' in options && options.isFile; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 0f862a7bc0b6..03201ae0ac08 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -156,6 +156,18 @@ export async function isCondaEnvironment(interpreterPathOrEnvPath: string): Prom return false; } +/** + * Gets path to conda's `environments.txt` file. More info https://github.com/conda/conda/issues/11845. + */ +export async function getCondaEnvironmentsTxt(): Promise { + const homeDir = getUserHomeDir(); + if (!homeDir) { + return []; + } + const environmentsTxt = path.join(homeDir, '.conda', 'environments.txt'); + return [environmentsTxt]; +} + /** * Extracts version information from `conda-meta/history` near a given interpreter. * @param interpreterPath Absolute path to the interpreter diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 68a61f93a7a2..a3b8c3f0aaf7 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -80,10 +80,6 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { } @injectable() class ComponentAdapter implements IComponentAdapter { - private readonly refreshing = new vscode.EventEmitter(); - - private readonly refreshed = new vscode.EventEmitter(); - private readonly changed = new vscode.EventEmitter(); constructor( @@ -135,15 +131,6 @@ class ComponentAdapter implements IComponentAdapter { }); } - // Implements IInterpreterLocatorProgressHandler - public get onRefreshing(): vscode.Event { - return this.refreshing.event; - } - - public get onRefreshed(): vscode.Event { - return this.refreshed.event; - } - // Implements IInterpreterHelper public async getInterpreterInformation(pythonPath: string): Promise | undefined> { const env = await this.api.resolveEnv(pythonPath); @@ -231,9 +218,6 @@ class ComponentAdapter implements IComponentAdapter { } public getInterpreters(resource?: vscode.Uri, source?: PythonEnvSource[]): PythonEnvironment[] { - // Notify locators are locating. - this.refreshing.fire(); - const query: PythonLocatorQuery = {}; let roots: vscode.Uri[] = []; let wsFolder: vscode.WorkspaceFolder | undefined; @@ -262,12 +246,7 @@ class ComponentAdapter implements IComponentAdapter { envs = envs.filter((env) => intersection(source, env.source).length > 0); } - const legacyEnvs = envs.map(convertEnvInfo); - - // Notify all locators have completed locating. Note it's crucial to notify this even when getInterpretersViaAPI - // fails, to ensure "Python extension loading..." text disappears. - this.refreshed.fire(); - return legacyEnvs; + return envs.map(convertEnvInfo); } public async getWorkspaceVirtualEnvInterpreters( diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..19db5d9f34b1 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { CondaEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/condaLocator'; +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 { traceWarn } from '../../../../../client/logging'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; + +class CondaEnvs { + private readonly condaEnvironmentsTxt; + + constructor() { + const home = platformUtils.getUserHomeDir(); + if (!home) { + throw new Error('Home directory not found'); + } + this.condaEnvironmentsTxt = path.join(home, '.conda', 'environments.txt'); + } + + public async create(): Promise { + try { + await fs.createFile(this.condaEnvironmentsTxt); + } catch (err) { + throw new Error(`Failed to create environments.txt ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async update(): Promise { + try { + await fs.writeFile(this.condaEnvironmentsTxt, 'path/to/environment'); + } catch (err) { + throw new Error(`Failed to update environments file ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async cleanUp() { + try { + await fs.remove(this.condaEnvironmentsTxt); + } catch (err) { + traceWarn(`Failed to clean up ${this.condaEnvironmentsTxt}`); + } + } +} + +suite('Conda Env Watcher', async () => { + let locator: CondaEnvironmentLocator; + let condaEnvsTxt: CondaEnvs; + + async function waitForChangeToBeDetected(deferred: Deferred) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + setup(async () => { + sinon.stub(platformUtils, 'getUserHomeDir').returns(TEST_LAYOUT_ROOT); + condaEnvsTxt = new CondaEnvs(); + await condaEnvsTxt.cleanUp(); + }); + + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise) { + locator = new CondaEnvironmentLocator(); + // Wait for watchers to get ready + await sleep(1000); + locator.onChanged(onChanged); + } + + teardown(async () => { + await condaEnvsTxt.cleanUp(); + await locator.dispose(); + sinon.restore(); + }); + + test('Fires when conda `environments.txt` file is created', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred(); + const expectedEvent = {}; + await setupLocator(async (e) => { + deferred.resolve(); + actualEvent = e; + }); + + await condaEnvsTxt.create(); + await waitForChangeToBeDetected(deferred); + + assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); + }); + + test('Fires when conda `environments.txt` file is updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred(); + const expectedEvent = {}; + await condaEnvsTxt.create(); + await setupLocator(async (e) => { + deferred.resolve(); + actualEvent = e; + }); + + await condaEnvsTxt.update(); + await waitForChangeToBeDetected(deferred); + + assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); + }); +}); From e23240eeea05f8830bd04b3812be121efde17177 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 26 Sep 2022 13:54:20 -0700 Subject: [PATCH 43/59] Add cancellation token support and link to logs. (#19882) Closes #19879 --- src/client/common/utils/localize.ts | 3 ++- .../creation/common/workspaceSelection.ts | 17 +++++++++++------ .../creation/createEnvironment.ts | 5 +++-- .../creation/provider/condaCreationProvider.ts | 4 ++-- .../creation/provider/condaUtils.ts | 14 +++++++++----- .../creation/provider/venvCreationProvider.ts | 2 +- .../creation/createEnvironment.unit.test.ts | 5 +++-- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 412fe412f3d8..0ce9d5420c32 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -96,6 +96,7 @@ export namespace Common { export const openLaunch = localize('Common.openLaunch', 'Open launch.json'); export const useCommandPrompt = localize('Common.useCommandPrompt', 'Use Command Prompt'); export const download = localize('Common.download', 'Download'); + export const showLogs = localize('Common.showLogs', 'Show logs'); } export namespace CommonSurvey { @@ -269,7 +270,7 @@ export namespace Interpreters { ); export const environmentPromptMessage = localize( 'Interpreters.environmentPromptMessage', - 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?', + 'We noticed a new environment has been created. Do you want to select it for the workspace folder?', ); export const entireWorkspace = localize('Interpreters.entireWorkspace', 'Select at workspace level'); export const clearAtWorkspace = localize('Interpreters.clearAtWorkspace', 'Clear at workspace level'); diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index 810d51856bb8..6aaee99e1f36 100644 --- a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -3,7 +3,7 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; -import { QuickPickItem, WorkspaceFolder } from 'vscode'; +import { CancellationToken, QuickPickItem, WorkspaceFolder } from 'vscode'; import { showErrorMessage, showQuickPick } from '../../../common/vscodeApis/windowApis'; import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; import { CreateEnv } from '../../../common/utils/localize'; @@ -30,6 +30,7 @@ async function getWorkspacesForQuickPick(workspaces: readonly WorkspaceFolder[]) export interface PickWorkspaceFolderOptions { allowMultiSelect?: boolean; + token?: CancellationToken; } export async function pickWorkspaceFolder( @@ -47,11 +48,15 @@ export async function pickWorkspaceFolder( } // This is multi-root scenario. - const selected = await showQuickPick(getWorkspacesForQuickPick(workspaces), { - title: CreateEnv.pickWorkspaceTitle, - ignoreFocusOut: true, - canPickMany: options?.allowMultiSelect, - }); + const selected = await showQuickPick( + getWorkspacesForQuickPick(workspaces), + { + title: CreateEnv.pickWorkspaceTitle, + ignoreFocusOut: true, + canPickMany: options?.allowMultiSelect, + }, + options?.token, + ); if (selected) { if (options?.allowMultiSelect) { diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index dc6dc9461cfd..a07555273604 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -5,7 +5,8 @@ import { CancellationToken, ProgressLocation } from 'vscode'; import { withProgress } from '../../common/vscodeApis/windowApis'; import { traceError } from '../../logging'; import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from './types'; -import { CreateEnv } from '../../common/utils/localize'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { Commands } from '../../common/constants'; export async function createEnvironment( provider: CreateEnvironmentProvider, @@ -17,7 +18,7 @@ export async function createEnvironment( return withProgress( { location: ProgressLocation.Notification, - title: CreateEnv.statusTitle, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, cancellable: true, }, async (progress: CreateEnvironmentProgress, token: CancellationToken) => { diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 81ef43334449..38da9038c59a 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -155,7 +155,7 @@ async function createEnvironment( progress?.report({ message: CreateEnv.Conda.waitingForWorkspace, }); - const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; + const workspace = (await pickWorkspaceFolder({ token })) as WorkspaceFolder | undefined; if (!workspace) { traceError('Workspace was not selected or found for creating virtual env.'); return undefined; @@ -164,7 +164,7 @@ async function createEnvironment( progress?.report({ message: CreateEnv.Conda.waitingForPython, }); - const version = await pickPythonVersion(); + const version = await pickPythonVersion(token); if (!version) { traceError('Conda environments for use with python extension require Python.'); return undefined; diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index fd35c82267ac..9496d01a07fe 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { QuickPickItem, Uri } from 'vscode'; +import { CancellationToken, QuickPickItem, Uri } from 'vscode'; import { Common } from '../../../browser/localize'; import { CreateEnv } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; @@ -21,13 +21,17 @@ export async function getConda(): Promise { return conda.command; } -export async function pickPythonVersion(): Promise { +export async function pickPythonVersion(token?: CancellationToken): Promise { const items: QuickPickItem[] = ['3.7', '3.8', '3.9', '3.10'].map((v) => ({ label: `Python`, description: v, })); - const version = await showQuickPick(items, { - title: CreateEnv.Conda.selectPythonQuickPickTitle, - }); + const version = await showQuickPick( + items, + { + title: CreateEnv.Conda.selectPythonQuickPickTitle, + }, + token, + ); return version?.description; } diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index f6ad31374eb3..fbdc73f39258 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -116,7 +116,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { message: CreateEnv.Venv.waitingForWorkspace, }); - const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; + const workspace = (await pickWorkspaceFolder({ token })) as WorkspaceFolder | undefined; if (workspace === undefined) { traceError('Workspace was not selected or found for creating virtual environment.'); return undefined; diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts index f9c12d1f6eb0..0e94e81ab38a 100644 --- a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -6,13 +6,14 @@ import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import { ProgressLocation, ProgressOptions } from 'vscode'; -import { CreateEnv } from '../../../client/common/utils/localize'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; import { createEnvironment } from '../../../client/pythonEnvironments/creation/createEnvironment'; import { CreateEnvironmentProgress, CreateEnvironmentProvider, } from '../../../client/pythonEnvironments/creation/types'; +import { Commands } from '../../../client/common/constants'; chaiUse(chaiAsPromised); @@ -26,7 +27,7 @@ suite('Create Environments Tests', () => { withProgressStub.callsFake(async (options: ProgressOptions, task) => { assert.deepEqual(options, { location: ProgressLocation.Notification, - title: CreateEnv.statusTitle, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, cancellable: true, }); From 0f045780e58e190e57411f4ac1227cda97c4fde2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 16:40:23 -0700 Subject: [PATCH 44/59] Finalizing design of proposed API for python environments (#19841) Closes https://github.com/microsoft/vscode-python/issues/19101 closes #18973 Co-authored-by: Karthik Nadig --- .eslintignore | 1 - package-lock.json | 18 +- package.json | 4 +- src/client/apiTypes.ts | 136 ---- src/client/common/application/extensions.ts | 69 +- src/client/common/types.ts | 5 + src/client/deprecatedProposedApi.ts | 161 +++++ src/client/deprecatedProposedApiTypes.ts | 142 ++++ src/client/extension.ts | 5 +- src/client/interpreter/interpreterService.ts | 4 +- src/client/proposedApi.ts | 386 ++++++++--- src/client/proposedApiTypes.ts | 262 ++++++++ src/client/pythonEnvironments/api.ts | 5 + src/client/pythonEnvironments/base/locator.ts | 1 + .../locators/composite/envsCollectionCache.ts | 12 - .../composite/envsCollectionService.ts | 3 + src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 25 + .../interpreterService.unit.test.ts | 8 +- src/test/proposedApi.unit.test.ts | 627 +++++++++--------- .../envsCollectionService.unit.test.ts | 26 - 21 files changed, 1309 insertions(+), 592 deletions(-) create mode 100644 src/client/deprecatedProposedApi.ts create mode 100644 src/client/deprecatedProposedApiTypes.ts create mode 100644 src/client/proposedApiTypes.ts diff --git a/.eslintignore b/.eslintignore index 2f759e89c516..7ba146c7d8e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -246,7 +246,6 @@ src/client/common/application/languageService.ts src/client/common/application/clipboard.ts src/client/common/application/workspace.ts src/client/common/application/debugSessionTelemetry.ts -src/client/common/application/extensions.ts src/client/common/application/documentManager.ts src/client/common/application/debugService.ts src/client/common/application/commands/reloadCommand.ts diff --git a/package-lock.json b/package-lock.json index be50eac6b1f9..8bd10ba6f104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", "semver": "^5.5.0", + "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", "uint64be": "^3.0.0", @@ -65,6 +66,7 @@ "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", + "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", "@types/vscode": "~1.68.0", @@ -833,6 +835,12 @@ "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", "dev": true }, + "node_modules/@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, "node_modules/@types/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", @@ -12554,7 +12562,6 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true, "engines": { "node": "*" } @@ -15989,6 +15996,12 @@ "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", "dev": true }, + "@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, "@types/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", @@ -25224,8 +25237,7 @@ "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" }, "static-extend": { "version": "0.1.2", diff --git a/package.json b/package.json index 1d7a09308eed..0f9f3e97cb2a 100644 --- a/package.json +++ b/package.json @@ -1816,6 +1816,7 @@ "request-progress": "^3.0.0", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", + "stack-trace": "0.0.10", "semver": "^5.5.0", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", @@ -1852,11 +1853,12 @@ "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", + "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", "@types/vscode": "~1.68.0", - "@types/winreg": "^1.2.30", "@types/which": "^2.0.1", + "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", diff --git a/src/client/apiTypes.ts b/src/client/apiTypes.ts index 6361a75edb48..a10fd2dccb96 100644 --- a/src/client/apiTypes.ts +++ b/src/client/apiTypes.ts @@ -4,8 +4,6 @@ import { Event, Uri } from 'vscode'; import { Resource } from './common/types'; import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; -import { EnvPathType, PythonEnvKind } from './pythonEnvironments/base/info'; -import { GetRefreshEnvironmentsOptions, ProgressNotificationEvent } from './pythonEnvironments/base/locator'; /* * Do not introduce any breaking changes to this API. @@ -88,137 +86,3 @@ export interface IExtensionApi { registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; }; } - -export interface EnvironmentDetailsOptions { - useCache: boolean; -} - -export interface EnvironmentDetails { - interpreterPath: string; - envFolderPath?: string; - version: string[]; - environmentType: PythonEnvKind[]; - metadata: Record; -} - -export interface EnvironmentsChangedParams { - /** - * Path to environment folder or path to interpreter that uniquely identifies an environment. - * Virtual environments lacking an interpreter are identified by environment folder paths, - * whereas other envs can be identified using interpreter path. - */ - path?: string; - type: 'add' | 'remove' | 'update' | 'clear-all'; -} - -export interface ActiveEnvironmentChangedParams { - /** - * Path to environment folder or path to interpreter that uniquely identifies an environment. - * Virtual environments lacking an interpreter are identified by environment folder paths, - * whereas other envs can be identified using interpreter path. - */ - path: string; - resource?: Uri; -} - -export interface IProposedExtensionAPI { - environment: { - /** - * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. - */ - readonly onDidChangeExecutionDetails: Event; - /** - * Returns all the details the consumer needs to execute code within the selected environment, - * corresponding to the specified resource taking into account any workspace-specific settings - * for the workspace to which this resource belongs. - * @param {Resource} [resource] A resource for which the setting is asked for. - * * When no resource is provided, the setting scoped to the first workspace folder is returned. - * * If no folder is present, it returns the global setting. - * @returns {({ execCommand: string[] | undefined })} - */ - getExecutionDetails( - resource?: Resource, - ): Promise<{ - /** - * E.g of execution commands returned could be, - * * `['']` - * * `['']` - * * `['conda', 'run', 'python']` which is used to run from within Conda environments. - * or something similar for some other Python environments. - * - * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. - * Otherwise, join the items returned using space to construct the full execution command. - */ - execCommand: string[] | undefined; - }>; - /** - * Returns the path to the python binary selected by the user or as in the settings. - * This is just the path to the python binary, this does not provide activation or any - * other activation command. The `resource` if provided will be used to determine the - * python binary in a multi-root scenario. If resource is `undefined` then the API - * returns what ever is set for the workspace. - * @param resource : Uri of a file or workspace - */ - getActiveEnvironmentPath(resource?: Resource): Promise; - /** - * Returns details for the given interpreter. Details such as absolute interpreter path, - * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under - * metadata field. - * @param path : Full path to environment folder or interpreter whose details you need. - * @param options : [optional] - * * useCache : When true, cache is checked first for any data, returns even if there - * is partial data. - */ - getEnvironmentDetails( - path: string, - options?: EnvironmentDetailsOptions, - ): Promise; - /** - * Returns paths to environments that uniquely identifies an environment found by the extension - * at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it - * will *not* wait for the refresh to finish. This will return what is known so far. To get - * complete list `await` on promise returned by `getRefreshPromise()`. - * - * Virtual environments lacking an interpreter are identified by environment folder paths, - * whereas other envs can be identified using interpreter path. - */ - getEnvironmentPaths(): Promise; - /** - * Sets the active environment path for the python extension for the resource. Configuration target - * will always be the workspace folder. - * @param path : Full path to environment folder or interpreter to set. - * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace - * folder. - */ - setActiveEnvironment(path: string, resource?: Resource): Promise; - /** - * This API will re-trigger environment discovery. Extensions can wait on the returned - * promise to get the updated environment list. If there is a refresh already going on - * then it returns the promise for that refresh. - * @param options : [optional] - * * clearCache : When true, this will clear the cache before environment refresh - * is triggered. - */ - refreshEnvironment(): Promise; - /** - * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant - * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of - * the entire collection. - */ - readonly onRefreshProgress: Event; - /** - * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active - * refreshes going on. - */ - getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; - /** - * This event is triggered when the known environment list changes, like when a environment - * is found, existing environment is removed, or some details changed on an environment. - */ - onDidEnvironmentsChanged: Event; - /** - * This event is triggered when the active environment changes. - */ - onDidActiveEnvironmentChanged: Event; - }; -} diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index 359f31e15138..9d62e76d5da4 100644 --- a/src/client/common/application/extensions.ts +++ b/src/client/common/application/extensions.ts @@ -1,14 +1,25 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. 'use strict'; -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { Event, Extension, extensions } from 'vscode'; +import * as stacktrace from 'stack-trace'; +import * as path from 'path'; import { IExtensions } from '../types'; +import { IFileSystem } from '../platform/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; +/** + * Provides functions for tracking the list of extensions that VSCode has installed. + */ @injectable() export class Extensions implements IExtensions { + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any public get all(): readonly Extension[] { return extensions.all; } @@ -17,7 +28,59 @@ export class Extensions implements IExtensions { return extensions.onDidChange; } - public getExtension(extensionId: any) { + public getExtension(extensionId: string): Extension | undefined { return extensions.getExtension(extensionId); } + + /** + * Code borrowed from: + * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts + */ + public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + const { stack } = new Error(); + if (stack) { + const pythonExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep); + const frames = stack + .split('\n') + .map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1]; + } + return undefined; + }) + .filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot)) + .filter((item) => + this.all.some( + (ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath), + ), + ) as string[]; + stacktrace.parse(new Error('Ex')).forEach((item) => { + const fileName = item.getFileName(); + if (fileName && !fileName.toLowerCase().startsWith(pythonExtRoot)) { + frames.push(fileName); + } + }); + for (const frame of frames) { + // This file is from a different extension. Try to find its `package.json`. + let dirName = path.dirname(frame); + let last = frame; + while (dirName && dirName.length < last.length) { + const possiblePackageJson = path.join(dirName, 'package.json'); + if (await this.fs.pathExists(possiblePackageJson)) { + const text = await this.fs.readFile(possiblePackageJson); + try { + const json = JSON.parse(text); + return { extensionId: `${json.publisher}.${json.name}`, displayName: json.displayName }; + } catch { + // If parse fails, then not an extension. + } + } + last = dirName; + dirName = path.dirname(dirName); + } + } + } + return { extensionId: 'unknown', displayName: 'unknown' }; + } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 571a9a01b8a2..66c91b13444d 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -428,6 +428,11 @@ export interface IExtensions { * @return An extension or `undefined`. */ getExtension(extensionId: string): Extension | undefined; + + /** + * Determines which extension called into our extension code based on call stacks. + */ + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; } export const IBrowserService = Symbol('IBrowserService'); diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts new file mode 100644 index 000000000000..84340772901a --- /dev/null +++ b/src/client/deprecatedProposedApi.ts @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, EventEmitter } from 'vscode'; +import { arePathsSame } from './common/platform/fs-paths'; +import { IExtensions, IInterpreterPathService, Resource } from './common/types'; +import { + EnvironmentsChangedParams, + ActiveEnvironmentChangedParams, + EnvironmentDetailsOptions, + EnvironmentDetails, + DeprecatedProposedAPI, +} from './deprecatedProposedApiTypes'; +import { IInterpreterService } from './interpreter/contracts'; +import { IServiceContainer } from './ioc/types'; +import { PythonEnvInfo } from './pythonEnvironments/base/info'; +import { getEnvPath } from './pythonEnvironments/base/info/env'; +import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; + +const onDidInterpretersChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { + onDidInterpretersChangedEvent.fire(e); +} + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportActiveInterpreterChangedDeprecated(e: ActiveEnvironmentChangedParams): void { + onDidActiveInterpreterChangedEvent.fire(e); +} + +function getVersionString(env: PythonEnvInfo): string[] { + const ver = [`${env.version.major}`, `${env.version.minor}`, `${env.version.micro}`]; + if (env.version.release) { + ver.push(`${env.version.release}`); + if (env.version.sysVersion) { + ver.push(`${env.version.release}`); + } + } + return ver; +} + +/** + * Returns whether the path provided matches the environment. + * @param path Path to environment folder or path to interpreter that uniquely identifies an environment. + * @param env Environment to match with. + */ +function isEnvSame(path: string, env: PythonEnvInfo) { + return arePathsSame(path, env.location) || arePathsSame(path, env.executable.filename); +} + +export function buildDeprecatedProposedApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, +): DeprecatedProposedAPI { + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterService = serviceContainer.get(IInterpreterService); + const extensions = serviceContainer.get(IExtensions); + function sendApiTelemetry(apiName: string, warnLog = true) { + if (warnLog) { + console.warn('Extension is using deprecated python APIs which will be removed soon'); + } + extensions + .determineExtensionFromCallStack() + .then((info) => + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + displayName: info.displayName, + }), + ) + .ignoreErrors(); + } + + const proposed: DeprecatedProposedAPI = { + environment: { + async getExecutionDetails(resource?: Resource) { + sendApiTelemetry('getExecutionDetails'); + const env = await interpreterService.getActiveInterpreter(resource); + return env ? { execCommand: [env.path] } : { execCommand: undefined }; + }, + async getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentPath'); + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return getEnvPath(env.path, env.envPath); + }, + async getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise { + sendApiTelemetry('getEnvironmentDetails'); + let env: PythonEnvInfo | undefined; + if (options?.useCache) { + env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); + } + if (!env) { + env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + } + return { + interpreterPath: env.executable.filename, + envFolderPath: env.location.length ? env.location : undefined, + version: getVersionString(env), + environmentType: [env.kind], + metadata: { + sysPrefix: env.executable.sysPrefix, + bitness: env.arch, + project: env.searchLocation, + }, + }; + }, + getEnvironmentPaths() { + sendApiTelemetry('getEnvironmentPaths'); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + setActiveEnvironment(path: string, resource?: Resource): Promise { + sendApiTelemetry('setActiveEnvironment'); + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + async refreshEnvironment() { + sendApiTelemetry('refreshEnvironment'); + await discoveryApi.triggerRefresh(); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + sendApiTelemetry('getRefreshPromise'); + return discoveryApi.getRefreshPromise(options); + }, + get onDidChangeExecutionDetails() { + sendApiTelemetry('onDidChangeExecutionDetails', false); + return interpreterService.onDidChangeInterpreterConfiguration; + }, + get onDidEnvironmentsChanged() { + sendApiTelemetry('onDidEnvironmentsChanged', false); + return onDidInterpretersChangedEvent.event; + }, + get onDidActiveEnvironmentChanged() { + sendApiTelemetry('onDidActiveEnvironmentChanged', false); + return onDidActiveInterpreterChangedEvent.event; + }, + get onRefreshProgress() { + sendApiTelemetry('onRefreshProgress', false); + return discoveryApi.onProgress; + }, + }, + }; + return proposed; +} diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts new file mode 100644 index 000000000000..cf6c01f21219 --- /dev/null +++ b/src/client/deprecatedProposedApiTypes.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, Event } from 'vscode'; +import { Resource } from './proposedApiTypes'; +import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; +import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; + +export interface EnvironmentDetailsOptions { + useCache: boolean; +} + +export interface EnvironmentDetails { + interpreterPath: string; + envFolderPath?: string; + version: string[]; + environmentType: PythonEnvKind[]; + metadata: Record; +} + +export interface EnvironmentsChangedParams { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path?: string; + type: 'add' | 'remove' | 'update' | 'clear-all'; +} + +export interface ActiveEnvironmentChangedParams { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path: string; + resource?: Uri; +} + +/** + * @deprecated Use {@link ProposedExtensionAPI} instead. + */ +export interface DeprecatedProposedAPI { + /** + * @deprecated Use {@link ProposedExtensionAPI.environment} instead. This will soon be removed. + */ + environment: { + /** + * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + */ + readonly onDidChangeExecutionDetails: Event; + /** + * Returns all the details the consumer needs to execute code within the selected environment, + * corresponding to the specified resource taking into account any workspace-specific settings + * for the workspace to which this resource belongs. + * @param {Resource} [resource] A resource for which the setting is asked for. + * * When no resource is provided, the setting scoped to the first workspace folder is returned. + * * If no folder is present, it returns the global setting. + * @returns {({ execCommand: string[] | undefined })} + */ + getExecutionDetails( + resource?: Resource, + ): Promise<{ + /** + * E.g of execution commands returned could be, + * * `['']` + * * `['']` + * * `['conda', 'run', 'python']` which is used to run from within Conda environments. + * or something similar for some other Python environments. + * + * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. + * Otherwise, join the items returned using space to construct the full execution command. + */ + execCommand: string[] | undefined; + }>; + /** + * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. + */ + getActiveEnvironmentPath(resource?: Resource): Promise; + /** + * Returns details for the given interpreter. Details such as absolute interpreter path, + * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under + * metadata field. + * @param path : Full path to environment folder or interpreter whose details you need. + * @param options : [optional] + * * useCache : When true, cache is checked first for any data, returns even if there + * is partial data. + */ + getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise; + /** + * Returns paths to environments that uniquely identifies an environment found by the extension + * at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it + * will *not* wait for the refresh to finish. This will return what is known so far. To get + * complete list `await` on promise returned by `getRefreshPromise()`. + * + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + getEnvironmentPaths(): Promise; + /** + * Sets the active environment path for the python extension for the resource. Configuration target + * will always be the workspace folder. + * @param path : Full path to environment folder or interpreter to set. + * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace + * folder. + */ + setActiveEnvironment(path: string, resource?: Resource): Promise; + /** + * This API will re-trigger environment discovery. Extensions can wait on the returned + * promise to get the updated environment list. If there is a refresh already going on + * then it returns the promise for that refresh. + * @param options : [optional] + * * clearCache : When true, this will clear the cache before environment refresh + * is triggered. + */ + refreshEnvironment(): Promise; + /** + * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant + * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of + * the entire collection. + */ + readonly onRefreshProgress: Event; + /** + * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active + * refreshes going on. + */ + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + onDidEnvironmentsChanged: Event; + /** + * @deprecated Use {@link ProposedExtensionAPI.environment} `onDidChangeActiveEnvironmentId` instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event; + }; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 6069583489ef..04ad91f80c84 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -42,10 +42,11 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { IExtensionApi, IProposedExtensionAPI } from './apiTypes'; +import { IExtensionApi } from './apiTypes'; import { buildProposedApi } from './proposedApi'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; +import { ProposedExtensionAPI } from './proposedApiTypes'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -103,7 +104,7 @@ async function activateUnsafe( context: IExtensionContext, startupStopWatch: StopWatch, startupDurations: IStartupDurations, -): Promise<[IExtensionApi & IProposedExtensionAPI, Promise, IServiceContainer]> { +): Promise<[IExtensionApi & ProposedExtensionAPI, Promise, IServiceContainer]> { // Add anything that we got from initializing logs to dispose. context.subscriptions.push(...logDispose); diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index cc4bf786dd6d..50545558d721 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -11,7 +11,7 @@ import { Uri, } from 'vscode'; import '../common/extensions'; -import { IApplicationShell, IDocumentManager } from '../common/application/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { IConfigurationService, IDisposableRegistry, @@ -222,7 +222,7 @@ export class InterpreterService implements Disposable, IInterpreterService { this.didChangeInterpreterEmitter.fire(); reportActiveInterpreterChanged({ path: pySettings.pythonPath, - resource, + resource: this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource), }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index fc432efeb821..e85c4009d2c9 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -1,118 +1,334 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, EventEmitter } from 'vscode'; -import { - ActiveEnvironmentChangedParams, - EnvironmentDetails, - EnvironmentDetailsOptions, - EnvironmentsChangedParams, - IProposedExtensionAPI, -} from './apiTypes'; -import { arePathsSame } from './common/platform/fs-paths'; -import { IInterpreterPathService, Resource } from './common/types'; -import { IInterpreterService } from './interpreter/contracts'; +import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder } from 'vscode'; +import * as pathUtils from 'path'; +import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; +import { Architecture } from './common/utils/platform'; import { IServiceContainer } from './ioc/types'; -import { PythonEnvInfo } from './pythonEnvironments/base/info'; +import { + ActiveEnvironmentIdChangeEvent, + Environment, + EnvironmentsChangeEvent, + ProposedExtensionAPI, + ResolvedEnvironment, + RefreshOptions, + Resource, + EnvironmentType, + EnvironmentTools, + EnvironmentId, +} from './proposedApiTypes'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; -import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { IPythonExecutionFactory } from './common/process/types'; +import { traceError } from './logging'; +import { normCasePath } from './common/platform/fs-paths'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { + buildDeprecatedProposedApi, + reportActiveInterpreterChangedDeprecated, + reportInterpretersChanged, +} from './deprecatedProposedApi'; -const onDidInterpretersChangedEvent = new EventEmitter(); -export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { - onDidInterpretersChangedEvent.fire(e); -} +type ActiveEnvironmentChangeEvent = { + resource: WorkspaceFolder | undefined; + path: string; +}; -const onDidActiveInterpreterChangedEvent = new EventEmitter(); -export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangedParams): void { - onDidActiveInterpreterChangedEvent.fire(e); +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); + reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); } -function getVersionString(env: PythonEnvInfo): string[] { - const ver = [`${env.version.major}`, `${env.version.minor}`, `${env.version.micro}`]; - if (env.version.release) { - ver.push(`${env.version.release}`); - if (env.version.sysVersion) { - ver.push(`${env.version.release}`); - } - } - return ver; -} +const onEnvironmentsChanged = new EventEmitter(); +const environmentsReference = new Map(); /** - * Returns whether the path provided matches the environment. - * @param path Path to environment folder or path to interpreter that uniquely identifies an environment. - * @param env Environment to match with. + * Make all properties in T mutable. */ -function isEnvSame(path: string, env: PythonEnvInfo) { - return arePathsSame(path, env.location) || arePathsSame(path, env.executable.filename); +type Mutable = { + -readonly [P in keyof T]: Mutable; +}; + +export class EnvironmentReference implements Environment { + readonly id: string; + + constructor(public internal: Environment) { + this.id = internal.id; + } + + get executable() { + return Object.freeze(this.internal.executable); + } + + get environment() { + return Object.freeze(this.internal.environment); + } + + get version() { + return Object.freeze(this.internal.version); + } + + get tools() { + return Object.freeze(this.internal.tools); + } + + get path() { + return Object.freeze(this.internal.path); + } + + updateEnv(newInternal: Environment) { + this.internal = newInternal; + } +} + +function getEnvReference(e: Environment) { + let envClass = environmentsReference.get(e.id); + if (!envClass) { + envClass = new EnvironmentReference(e); + } else { + envClass.updateEnv(e); + } + environmentsReference.set(e.id, envClass); + return envClass; } export function buildProposedApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, -): IProposedExtensionAPI { +): ProposedExtensionAPI { const interpreterPathService = serviceContainer.get(IInterpreterPathService); - const interpreterService = serviceContainer.get(IInterpreterService); + const configService = serviceContainer.get(IConfigurationService); + const disposables = serviceContainer.get(IDisposableRegistry); + const extensions = serviceContainer.get(IExtensions); + function sendApiTelemetry(apiName: string) { + extensions + .determineExtensionFromCallStack() + .then((info) => + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + displayName: info.displayName, + }), + ) + .ignoreErrors(); + } + disposables.push( + discoveryApi.onChanged((e) => { + if (e.old) { + if (e.new) { + onEnvironmentsChanged.fire({ type: 'update', env: convertEnvInfoAndGetReference(e.new) }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'update', + }, + ]); + } else { + onEnvironmentsChanged.fire({ type: 'remove', env: convertEnvInfoAndGetReference(e.old) }); + reportInterpretersChanged([ + { + path: getEnvPath(e.old.executable.filename, e.old.location).path, + type: 'remove', + }, + ]); + } + } else if (e.new) { + onEnvironmentsChanged.fire({ type: 'add', env: convertEnvInfoAndGetReference(e.new) }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'add', + }, + ]); + } + }), + onEnvironmentsChanged, + ); + + /** + * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI.environment} instead. + */ + let deprecatedEnvironmentsApi; + try { + deprecatedEnvironmentsApi = { ...buildDeprecatedProposedApi(discoveryApi, serviceContainer).environment }; + } catch (ex) { + deprecatedEnvironmentsApi = {}; + // Errors out only in case of testing. + // Also, these APIs no longer supported, no need to log error. + } - const proposed: IProposedExtensionAPI = { + const proposed: ProposedExtensionAPI = { environment: { - async getExecutionDetails(resource?: Resource) { - const env = await interpreterService.getActiveInterpreter(resource); - return env ? { execCommand: [env.path] } : { execCommand: undefined }; + getActiveEnvironmentId(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentId'); + resource = resource && 'uri' in resource ? resource.uri : resource; + const path = configService.getSettings(resource).pythonPath; + const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); + return { id, path }; }, - async getActiveEnvironmentPath(resource?: Resource) { - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return getEnvPath(env.path, env.envPath); + updateActiveEnvironmentId(env: Environment | EnvironmentId | string, resource?: Resource): Promise { + sendApiTelemetry('updateActiveEnvironmentId'); + const path = typeof env !== 'string' ? env.path : env; + resource = resource && 'uri' in resource ? resource.uri : resource; + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - async getEnvironmentDetails( - path: string, - options?: EnvironmentDetailsOptions, - ): Promise { - let env: PythonEnvInfo | undefined; - if (options?.useCache) { - env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); - } - if (!env) { - env = await discoveryApi.resolveEnv(path); - if (!env) { + get onDidChangeActiveEnvironmentId() { + sendApiTelemetry('onDidChangeActiveEnvironmentId'); + return onDidActiveInterpreterChangedEvent.event; + }, + resolveEnvironment: async (env: Environment | EnvironmentId | string) => { + let path = typeof env !== 'string' ? env.path : env; + if (pathUtils.basename(path) === path) { + // Value can be `python`, `python3`, `python3.9` etc. + // This case could eventually be handled by the internal discovery API itself. + const pythonExecutionFactory = serviceContainer.get( + IPythonExecutionFactory, + ); + const pythonExecutionService = await pythonExecutionFactory.create({ pythonPath: path }); + const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch((ex) => { + traceError('Cannot resolve full path', ex); + return undefined; + }); + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { return undefined; } + path = fullyQualifiedPath; } - return { - interpreterPath: env.executable.filename, - envFolderPath: env.location.length ? env.location : undefined, - version: getVersionString(env), - environmentType: [env.kind], - metadata: { - sysPrefix: env.executable.sysPrefix, - bitness: env.arch, - project: env.searchLocation, - }, - }; - }, - getEnvironmentPaths() { - const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); - return Promise.resolve(paths); + sendApiTelemetry('resolveEnvironment'); + return resolveEnvironment(path, discoveryApi); }, - setActiveEnvironment(path: string, resource?: Resource): Promise { - return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + get all(): Environment[] { + sendApiTelemetry('all'); + return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, - async refreshEnvironment() { - await discoveryApi.triggerRefresh(); - const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); - return Promise.resolve(paths); + async refreshEnvironments(options?: RefreshOptions) { + await discoveryApi.triggerRefresh(undefined, { + ifNotTriggerredAlready: !options?.forceRefresh, + }); + sendApiTelemetry('refreshEnvironments'); }, - getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { - return discoveryApi.getRefreshPromise(options); + get onDidChangeEnvironments() { + sendApiTelemetry('onDidChangeEnvironments'); + return onEnvironmentsChanged.event; }, - onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, - onDidEnvironmentsChanged: onDidInterpretersChangedEvent.event, - onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedEvent.event, - onRefreshProgress: discoveryApi.onProgress, + ...deprecatedEnvironmentsApi, }, }; return proposed; } + +async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Promise { + const env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + return getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; +} + +export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { + const version = { ...env.version, sysVersion: env.version.sysVersion }; + let tool = convertKind(env.kind); + if (env.type && !tool) { + tool = 'Unknown'; + } + const { path } = getEnvPath(env.executable.filename, env.location); + const resolvedEnv: ResolvedEnvironment = { + path, + id: getEnvID(path), + executable: { + uri: Uri.file(env.executable.filename), + bitness: convertBitness(env.arch), + sysPrefix: env.executable.sysPrefix, + }, + environment: env.type + ? { + type: convertEnvType(env.type), + name: env.name, + folderUri: Uri.file(env.location), + workspaceFolder: env.searchLocation, + } + : undefined, + version: version as ResolvedEnvironment['version'], + tools: tool ? [tool] : [], + }; + return resolvedEnv; +} + +function convertEnvType(envType: PythonEnvType): EnvironmentType { + if (envType === PythonEnvType.Conda) { + return 'Conda'; + } + if (envType === PythonEnvType.Virtual) { + return 'VirtualEnvironment'; + } + return 'Unknown'; +} + +function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { + switch (kind) { + case PythonEnvKind.Venv: + return 'Venv'; + case PythonEnvKind.Pipenv: + return 'Pipenv'; + case PythonEnvKind.Poetry: + return 'Poetry'; + case PythonEnvKind.VirtualEnvWrapper: + return 'VirtualEnvWrapper'; + case PythonEnvKind.VirtualEnv: + return 'VirtualEnv'; + case PythonEnvKind.Conda: + return 'Conda'; + case PythonEnvKind.Pyenv: + return 'Pyenv'; + default: + return undefined; + } +} + +export function convertEnvInfo(env: PythonEnvInfo): Environment { + const convertedEnv = convertCompleteEnvInfo(env) as Mutable; + if (convertedEnv.executable.sysPrefix === '') { + convertedEnv.executable.sysPrefix = undefined; + } + if (convertedEnv.executable.uri?.fsPath === 'python') { + convertedEnv.executable.uri = undefined; + } + if (convertedEnv.environment?.name === '') { + convertedEnv.environment.name = undefined; + } + if (convertedEnv.version.major === -1) { + convertedEnv.version.major = undefined; + } + if (convertedEnv.version.micro === -1) { + convertedEnv.version.micro = undefined; + } + if (convertedEnv.version.minor === -1) { + convertedEnv.version.minor = undefined; + } + return convertedEnv as Environment; +} + +function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { + return getEnvReference(convertEnvInfo(env)); +} + +function convertBitness(arch: Architecture) { + switch (arch) { + case Architecture.x64: + return '64-bit'; + case Architecture.x86: + return '32-bit'; + default: + return 'Unknown'; + } +} + +function getEnvID(path: string) { + return normCasePath(path); +} diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts new file mode 100644 index 000000000000..89f9f1e4e5f4 --- /dev/null +++ b/src/client/proposedApiTypes.ts @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; + +// https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs + +export interface ProposedExtensionAPI { + readonly environment: { + /** + * Returns the environment configured by user in settings. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentId(resource?: Resource): EnvironmentId; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass + * the environment itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentId( + environment: Environment | EnvironmentId | string, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentId: Event; + /** + * Carries environments found by the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + */ + readonly all: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass + * the environment id or the environment itself. + */ + resolveEnvironment(environment: Environment | EnvironmentId | string): Promise; + }; +} + +export type RefreshOptions = { + /** + * Force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentId & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: Uri | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment. + */ + readonly version: VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information. + */ + readonly version: ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentIdChangeEvent = EnvironmentId & { + /** + * Workspace folder the environment changed for. + */ + readonly resource: WorkspaceFolder | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentId = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; diff --git a/src/client/pythonEnvironments/api.ts b/src/client/pythonEnvironments/api.ts index b9c3152a0b67..a2065c30b740 100644 --- a/src/client/pythonEnvironments/api.ts +++ b/src/client/pythonEnvironments/api.ts @@ -6,6 +6,7 @@ import { GetRefreshEnvironmentsOptions, IDiscoveryAPI, ProgressNotificationEvent, + ProgressReportStage, PythonLocatorQuery, TriggerRefreshOptions, } from './base/locator'; @@ -33,6 +34,10 @@ class PythonEnvironments implements IDiscoveryAPI { return this.locator.onProgress; } + public get refreshState(): ProgressReportStage { + return this.locator.refreshState; + } + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { return this.locator.getRefreshPromise(options); } diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index c0d1cd23991c..687348964891 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -201,6 +201,7 @@ export type TriggerRefreshOptions = { }; export interface IDiscoveryAPI { + readonly refreshState: ProgressReportStage; /** * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index 14663e2d117d..a8820a0f82b8 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -4,7 +4,6 @@ import { Event } from 'vscode'; import { isTestExecution } from '../../../../common/constants'; import { traceInfo } from '../../../../logging'; -import { reportInterpretersChanged } from '../../../../proposedApi'; import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; import { PythonEnvInfo } from '../../info'; import { areSameEnv, getEnvPath } from '../../info/env'; @@ -113,9 +112,6 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { const env = this.envs.splice(index, 1)[0]; this.fire({ old: env, new: undefined }); - reportInterpretersChanged([ - { path: getEnvPath(env.executable.filename, env.location).path, type: 'remove' }, - ]); }); } @@ -132,7 +128,6 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { this.fire({ old: e, new: undefined }); }); - reportInterpretersChanged([{ path: undefined, type: 'clear-all' }]); this.envs = []; return Promise.resolve(); } diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 0e1466bc385d..ca7c93b1c269 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -43,6 +43,8 @@ export class EnvsCollectionService extends PythonEnvsWatcher(); + public refreshState = ProgressReportStage.discoveryFinished; + public get onProgress(): Event { return this.progress.event; } @@ -70,6 +72,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher { + this.refreshState = event.stage; // Resolve progress promise indicating the stage has been reached. this.progressPromises.get(event.stage)?.resolve(); this.progressPromises.delete(event.stage); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 2ab6c8a8a3ba..4a611fcf3e7f 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -19,6 +19,7 @@ export enum EventName { PYTHON_INTERPRETER = 'PYTHON_INTERPRETER', PYTHON_INSTALL_PACKAGE = 'PYTHON_INSTALL_PACKAGE', ENVIRONMENT_WITHOUT_PYTHON_SELECTED = 'ENVIRONMENT_WITHOUT_PYTHON_SELECTED', + PYTHON_ENVIRONMENTS_API = 'PYTHON_ENVIRONMENTS_API', PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION', PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f8c443ff58ce..7211aa38e54f 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1107,6 +1107,31 @@ export interface IEventNamePropertyMapping { */ discovered: boolean; }; + + /** + * Telemetry event sent when another extension calls into python extension's environment API. Contains details + * of the other extension. + */ + /* __GDPR__ + "python_environments_api" : { + "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false , "owner": "karrtikr"}, + "displayName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false, "owner": "karrtikr" } + } + */ + [EventName.PYTHON_ENVIRONMENTS_API]: { + /** + * The ID of the extension calling the API. + */ + extensionId: string; + /** + * The name of the extension as displayed in marketplace. + */ + displayName: string; + /** + * The name of the API called. + */ + apiName: string; + }; /** * Telemetry event sent with details after updating the python interpreter */ diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 4ab2c8086309..1bbf729e53b9 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -248,6 +248,8 @@ suite('Interpreters service', () => { test('If stored setting is an empty string, refresh the interpreter display', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); service._pythonPathSetting = ''; configService.reset(); configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); @@ -259,13 +261,15 @@ suite('Interpreters service', () => { interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { path: 'current path', - resource, + resource: workspaceFolder, }); }); test('If stored setting is not equal to current interpreter path setting, refresh the interpreter display', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); service._pythonPathSetting = 'stored setting'; configService.reset(); configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); @@ -277,7 +281,7 @@ suite('Interpreters service', () => { interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { path: 'current path', - resource, + resource: workspaceFolder, }); }); diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index ad4cdc904a22..816bf1051d25 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -3,404 +3,393 @@ import * as typemoq from 'typemoq'; import { assert, expect } from 'chai'; -import { ConfigurationTarget, Uri, Event } from 'vscode'; -import { EnvironmentDetails, IProposedExtensionAPI } from '../client/apiTypes'; -import { IInterpreterPathService } from '../client/common/types'; -import { IInterpreterService } from '../client/interpreter/contracts'; +import { Uri, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode'; +import { cloneDeep } from 'lodash'; +import { + IConfigurationService, + IDisposableRegistry, + IExtensions, + IInterpreterPathService, + IPythonSettings, +} from '../client/common/types'; import { IServiceContainer } from '../client/ioc/types'; -import { buildProposedApi } from '../client/proposedApi'; import { - IDiscoveryAPI, - ProgressNotificationEvent, - ProgressReportStage, -} from '../client/pythonEnvironments/base/locator'; -import { PythonEnvironment } from '../client/pythonEnvironments/info'; + buildProposedApi, + convertCompleteEnvInfo, + convertEnvInfo, + EnvironmentReference, + reportActiveInterpreterChanged, +} from '../client/proposedApi'; +import { IDiscoveryAPI, ProgressNotificationEvent } from '../client/pythonEnvironments/base/locator'; +import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; +import { sleep } from './core'; import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; import { Architecture } from '../client/common/utils/platform'; -import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; +import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/base/watcher'; +import { + ProposedExtensionAPI, + ActiveEnvironmentIdChangeEvent, + EnvironmentsChangeEvent, +} from '../client/proposedApiTypes'; +import { normCasePath } from '../client/common/platform/fs-paths'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; let discoverAPI: typemoq.IMock; let interpreterPathService: typemoq.IMock; - let interpreterService: typemoq.IMock; - let onDidExecutionEvent: Event; - let onRefreshProgress: Event; + let configService: typemoq.IMock; + let extensions: typemoq.IMock; + let onDidChangeRefreshState: EventEmitter; + let onDidChangeEnvironments: EventEmitter; - let proposed: IProposedExtensionAPI; + let proposed: ProposedExtensionAPI; setup(() => { - serviceContainer = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - discoverAPI = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - interpreterPathService = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - interpreterService = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - onDidExecutionEvent = typemoq.Mock.ofType>().object; - onRefreshProgress = typemoq.Mock.ofType>().object; - interpreterService.setup((i) => i.onDidChangeInterpreterConfiguration).returns(() => onDidExecutionEvent); - + serviceContainer = typemoq.Mock.ofType(); + discoverAPI = typemoq.Mock.ofType(); + extensions = typemoq.Mock.ofType(); + extensions + .setup((e) => e.determineExtensionFromCallStack()) + .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })) + .verifiable(typemoq.Times.atLeastOnce()); + interpreterPathService = typemoq.Mock.ofType(); + configService = typemoq.Mock.ofType(); + onDidChangeRefreshState = new EventEmitter(); + onDidChangeEnvironments = new EventEmitter(); + + serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); - serviceContainer.setup((s) => s.get(IInterpreterService)).returns(() => interpreterService.object); + serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); + serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); - discoverAPI.setup((d) => d.onProgress).returns(() => onRefreshProgress); + discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); + discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); proposed = buildProposedApi(discoverAPI.object, serviceContainer.object); }); - test('Provide a callback for tracking refresh progress', async () => { - assert.deepEqual(proposed.environment.onRefreshProgress, onRefreshProgress); + teardown(() => { + // Verify each API method sends telemetry regarding who called the API. + extensions.verifyAll(); }); - test('Provide a callback which is called when execution details changes', async () => { - assert.deepEqual(onDidExecutionEvent, proposed.environment.onDidChangeExecutionDetails); + test('Provide an event to track when active environment details change', async () => { + const events: ActiveEnvironmentIdChangeEvent[] = []; + proposed.environment.onDidChangeActiveEnvironmentId((e) => { + events.push(e); + }); + reportActiveInterpreterChanged({ path: 'path/to/environment', resource: undefined }); + await sleep(1); + assert.deepEqual(events, [ + { id: normCasePath('path/to/environment'), path: 'path/to/environment', resource: undefined }, + ]); }); - test('getExecutionDetails: No resource', async () => { + test('getActiveEnvironmentId: No resource', () => { const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(undefined)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getExecutionDetails(); - assert.deepEqual(actual, { execCommand: [pythonPath] }); + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environment.getActiveEnvironmentId(); + assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath }); }); - test('getExecutionDetails: With resource', async () => { - const resource = Uri.file(__filename); - const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(resource)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getExecutionDetails(resource); - assert.deepEqual(actual, { execCommand: [pythonPath] }); + test('getActiveEnvironmentId: default python', () => { + const pythonPath = 'python'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environment.getActiveEnvironmentId(); + assert.deepEqual(actual, { id: 'DEFAULT_PYTHON', path: pythonPath }); }); - test('getActiveInterpreterPath: No resource', async () => { + test('getActiveEnvironmentId: With resource', () => { const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(undefined)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getActiveEnvironmentPath(); - assert.deepEqual(actual, { path: pythonPath, pathType: 'interpreterPath' }); - }); - test('getActiveInterpreterPath: With resource', async () => { const resource = Uri.file(__filename); - const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(resource)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getActiveEnvironmentPath(resource); - assert.deepEqual(actual, { path: pythonPath, pathType: 'interpreterPath' }); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environment.getActiveEnvironmentId(resource); + assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath }); }); - test('getInterpreterDetails: no discovered python', async () => { - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + test('resolveEnvironment: invalid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(undefined)); - const pythonPath = 'this/is/a/test/path (without cache)'; - const actual = await proposed.environment.getEnvironmentDetails(pythonPath); + const actual = await proposed.environment.resolveEnvironment(pythonPath); expect(actual).to.be.equal(undefined); }); - test('getInterpreterDetails: no discovered python (with cache)', async () => { - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); - + test('resolveEnvironment: valid environment (when passed as string)', async () => { const pythonPath = 'this/is/a/test/path'; - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - expect(actual).to.be.equal(undefined); + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + + const actual = await proposed.environment.resolveEnvironment(pythonPath); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); - test('getInterpreterDetails: without cache', async () => { + test('resolveEnvironment: valid environment (when passed as environment)', async () => { const pythonPath = 'this/is/a/test/path'; - - const expected: EnvironmentDetails = { - interpreterPath: pythonPath, - version: ['3', '9', '0'], - environmentType: [PythonEnvKind.System], - metadata: { - sysPrefix: 'prefix/path', - bitness: Architecture.x64, - project: Uri.file('path/to/project'), + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, }, - envFolderPath: undefined, - }; + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }); + const partialEnv = buildEnvInfo({ + executable: pythonPath, + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + + const actual = await proposed.environment.resolveEnvironment(convertCompleteEnvInfo(partialEnv)); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); + test('environments: no pythons found', () => { discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI - .setup((p) => p.resolveEnv(pythonPath)) - .returns(() => - Promise.resolve( - buildEnvInfo({ - executable: pythonPath, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - sysPrefix: 'prefix/path', - searchLocation: Uri.file('path/to/project'), - }), - ), - ); - - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: false }); - expect(actual).to.be.deep.equal(expected); + const actual = proposed.environment.all; + expect(actual).to.be.deep.equal([]); }); - test('getInterpreterDetails: from cache', async () => { - const pythonPath = 'this/is/a/test/path'; + test('environments: python found', async () => { + const envs = [ + { + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: -1, + micro: -1, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); + const actual = proposed.environment.all; + const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); + assert.deepEqual( + actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)), + envs.map((e) => convertEnvInfo(e)).sort((a, b) => a.id.localeCompare(b.id)), + ); + }); - const expected: EnvironmentDetails = { - interpreterPath: pythonPath, - version: ['3', '9', '0'], - environmentType: [PythonEnvKind.System], - metadata: { + test('Provide an event to track when list of environments change', async () => { + let events: EnvironmentsChangeEvent[] = []; + let eventValues: EnvironmentsChangeEvent[] = []; + let expectedEvents: EnvironmentsChangeEvent[] = []; + proposed.environment.onDidChangeEnvironments((e) => { + events.push(e); + }); + const envs = [ + buildEnvInfo({ + executable: 'pythonPath', + kind: PythonEnvKind.System, sysPrefix: 'prefix/path', - bitness: Architecture.x64, - project: undefined, + searchLocation: Uri.file('path/to/project'), + }), + { + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, }, - envFolderPath: undefined, - }; - - discoverAPI - .setup((d) => d.getEnvs()) - .returns(() => [ - { - executable: { - filename: pythonPath, - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, + { + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', }, - ]); - discoverAPI - .setup((p) => p.resolveEnv(pythonPath)) - .returns(() => - Promise.resolve( - buildEnvInfo({ - executable: pythonPath, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - sysPrefix: 'prefix/path', - }), - ), - ); - - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - expect(actual).to.be.deep.equal(expected); + version: { + major: 3, + minor: 10, + micro: 0, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + + // Now fire and verify events. Note the event value holds the reference to an environment, so may itself + // change when the environment is altered. So it's important to verify them as soon as they're received. + + // Add events + onDidChangeEnvironments.fire({ old: undefined, new: envs[0] }); + expectedEvents.push({ env: convertEnvInfo(envs[0]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[1] }); + expectedEvents.push({ env: convertEnvInfo(envs[1]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[2] }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'add' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Update events + events = []; + expectedEvents = []; + const updatedEnv = cloneDeep(envs[0]); + updatedEnv.arch = Architecture.x86; + onDidChangeEnvironments.fire({ old: envs[0], new: updatedEnv }); + expectedEvents.push({ env: convertEnvInfo(updatedEnv), type: 'update' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Remove events + events = []; + expectedEvents = []; + onDidChangeEnvironments.fire({ old: envs[2], new: undefined }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'remove' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); }); - test('getInterpreterDetails: cache miss', async () => { - const pythonPath = 'this/is/a/test/path'; + test('updateActiveEnvironmentId: no resource', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); - const expected: EnvironmentDetails = { - interpreterPath: pythonPath, - version: ['3', '9', '0'], - environmentType: [PythonEnvKind.System], - metadata: { - sysPrefix: 'prefix/path', - bitness: Architecture.x64, - project: undefined, - }, - envFolderPath: undefined, - }; + await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path'); - // Force this API to return empty to cause a cache miss. - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI - .setup((p) => p.resolveEnv(pythonPath)) - .returns(() => - Promise.resolve( - buildEnvInfo({ - executable: pythonPath, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - sysPrefix: 'prefix/path', - }), - ), - ); - - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - expect(actual).to.be.deep.equal(expected); + interpreterPathService.verifyAll(); }); - test('getInterpreterPaths: no pythons found', async () => { - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - const actual = await proposed.environment.getEnvironmentPaths(); - expect(actual).to.be.deep.equal([]); - }); + test('updateActiveEnvironmentId: passed as Environment', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); - test('getInterpreterPaths: python found', async () => { - discoverAPI - .setup((d) => d.getEnvs()) - .returns(() => [ - { - executable: { - filename: 'this/is/a/test/python/path1', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - { - executable: { - filename: 'this/is/a/test/python/path2', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 10, - micro: 0, - }, - kind: PythonEnvKind.Venv, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - ]); - const actual = await proposed.environment.getEnvironmentPaths(); - expect(actual?.map((a) => a.path)).to.be.deep.equal([ - 'this/is/a/test/python/path1', - 'this/is/a/test/python/path2', - ]); + await proposed.environment.updateActiveEnvironmentId({ + id: normCasePath('this/is/a/test/python/path'), + path: 'this/is/a/test/python/path', + }); + + interpreterPathService.verifyAll(); }); - test('setActiveInterpreter: no resource', async () => { + test('updateActiveEnvironmentId: with uri', async () => { + const uri = Uri.parse('a'); interpreterPathService - .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.setActiveEnvironment('this/is/a/test/python/path'); + await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path', uri); interpreterPathService.verifyAll(); }); - test('setActiveInterpreter: with resource', async () => { - const resource = Uri.parse('a'); + + test('updateActiveEnvironmentId: with workspace folder', async () => { + const uri = Uri.parse('a'); interpreterPathService - .setup((i) => i.update(resource, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); + const workspace: WorkspaceFolder = { + uri, + name: '', + index: 0, + }; - await proposed.environment.setActiveEnvironment('this/is/a/test/python/path', resource); + await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path', workspace); interpreterPathService.verifyAll(); }); - test('refreshInterpreters: common scenario', async () => { + test('refreshInterpreters: default', async () => { discoverAPI - .setup((d) => d.triggerRefresh(undefined, undefined)) + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: true }))) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - discoverAPI - .setup((d) => d.getEnvs()) - .returns(() => [ - { - executable: { - filename: 'this/is/a/test/python/path1', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - name: '', - location: 'this/is/a/test/python/path1/folder', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - { - executable: { - filename: 'this/is/a/test/python/path2', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 10, - micro: 0, - }, - kind: PythonEnvKind.Venv, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - ]); - const actual = await proposed.environment.refreshEnvironment(); - expect(actual).to.be.deep.equal([ - { path: 'this/is/a/test/python/path1/folder', pathType: 'envFolderPath' }, - { path: 'this/is/a/test/python/path2', pathType: 'interpreterPath' }, - ]); + await proposed.environment.refreshEnvironments(); + discoverAPI.verifyAll(); }); - test('getRefreshPromise: common scenario', () => { - const expected = Promise.resolve(); + test('refreshInterpreters: when forcing a refresh', async () => { discoverAPI - .setup((d) => d.getRefreshPromise(typemoq.It.isValue({ stage: ProgressReportStage.allPathsDiscovered }))) - .returns(() => expected); - const actual = proposed.environment.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); - - // We are comparing instances here, they should be the same instance. - // So '==' is ok here. - // eslint-disable-next-line eqeqeq - expect(actual == expected).is.equal(true); + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: false }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await proposed.environment.refreshEnvironments({ forceRefresh: true }); + + discoverAPI.verifyAll(); }); }); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts index 7bb70bead0a9..90dcb8345732 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -8,7 +8,6 @@ import * as sinon from 'sinon'; import { EventEmitter, Uri } from 'vscode'; import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; import { createDeferred, createDeferredFromPromise, sleep } from '../../../../../client/common/utils/async'; -import * as proposedApi from '../../../../../client/proposedApi'; import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; import { buildEnvInfo } from '../../../../../client/pythonEnvironments/base/info/env'; import { @@ -31,7 +30,6 @@ import { assertEnvEqual, assertEnvsEqual } from '../envTestUtils'; suite('Python envs locator - Environments Collection', async () => { let collectionService: EnvsCollectionService; let storage: PythonEnvInfo[]; - let reportInterpretersChangedStub: sinon.SinonStub; const updatedName = 'updatedName'; @@ -129,7 +127,6 @@ suite('Python envs locator - Environments Collection', async () => { }, }); collectionService = new EnvsCollectionService(cache, parentLocator); - reportInterpretersChangedStub = sinon.stub(proposedApi, 'reportInterpretersChanged'); }); teardown(() => { @@ -377,7 +374,6 @@ suite('Python envs locator - Environments Collection', async () => { collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, env); - sinon.assert.calledOnce(reportInterpretersChangedStub); }); test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { @@ -406,7 +402,6 @@ suite('Python envs locator - Environments Collection', async () => { collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); - sinon.assert.calledOnce(reportInterpretersChangedStub); }); test('resolveEnv() uses underlying locator if cache does not have complete info for env', async () => { @@ -429,22 +424,6 @@ suite('Python envs locator - Environments Collection', async () => { collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); - - const eventData = [ - { - path: path.join(TEST_LAYOUT_ROOT, 'doesNotExist'), - type: 'remove', - }, - - { - path: 'Resolved via locator', - type: 'add', - }, - ]; - eventData.forEach((d) => { - sinon.assert.calledWithExactly(reportInterpretersChangedStub, [d]); - }); - sinon.assert.callCount(reportInterpretersChangedStub, eventData.length); }); test('resolveEnv() adds env to cache after resolving using downstream locator', async () => { @@ -468,9 +447,6 @@ suite('Python envs locator - Environments Collection', async () => { const envs = collectionService.getEnvs(); expect(resolved?.hasLatestInfo).to.equal(true); assertEnvsEqual(envs, [resolved]); - sinon.assert.calledOnceWithExactly(reportInterpretersChangedStub, [ - { path: resolved?.executable.filename, type: 'add' }, - ]); }); test('Ensure events from downstream locators do not trigger new refreshes if a refresh is already scheduled', async () => { @@ -523,7 +499,5 @@ suite('Python envs locator - Environments Collection', async () => { events.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), downstreamEvents.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), ); - - sinon.assert.notCalled(reportInterpretersChangedStub); }); }); From 32b688efe48d23b887fa953238a55801a98d10d2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 22:17:03 -0700 Subject: [PATCH 45/59] Rename `EnvironmentId` to `EnvironmentPath` (#19887) --- src/client/deprecatedProposedApi.ts | 8 ---- src/client/deprecatedProposedApiTypes.ts | 6 +-- src/client/proposedApi.ts | 32 ++++++++----- src/client/proposedApiTypes.ts | 18 ++++---- src/test/proposedApi.unit.test.ts | 57 +++++++++++++++--------- 5 files changed, 67 insertions(+), 54 deletions(-) diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index 84340772901a..ef4d8a1d5eaa 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -85,14 +85,6 @@ export function buildDeprecatedProposedApi( const env = await interpreterService.getActiveInterpreter(resource); return env ? { execCommand: [env.path] } : { execCommand: undefined }; }, - async getActiveEnvironmentPath(resource?: Resource) { - sendApiTelemetry('getActiveEnvironmentPath'); - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return getEnvPath(env.path, env.envPath); - }, async getEnvironmentDetails( path: string, options?: EnvironmentDetailsOptions, diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index cf6c01f21219..4193c8b18212 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -74,10 +74,6 @@ export interface DeprecatedProposedAPI { */ execCommand: string[] | undefined; }>; - /** - * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. - */ - getActiveEnvironmentPath(resource?: Resource): Promise; /** * Returns details for the given interpreter. Details such as absolute interpreter path, * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under @@ -135,7 +131,7 @@ export interface DeprecatedProposedAPI { */ onDidEnvironmentsChanged: Event; /** - * @deprecated Use {@link ProposedExtensionAPI.environment} `onDidChangeActiveEnvironmentId` instead. This will soon be removed. + * @deprecated Use {@link ProposedExtensionAPI.environment} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. */ onDidActiveEnvironmentChanged: Event; }; diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index e85c4009d2c9..1147e839d0a5 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -8,7 +8,7 @@ import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPa import { Architecture } from './common/utils/platform'; import { IServiceContainer } from './ioc/types'; import { - ActiveEnvironmentIdChangeEvent, + ActiveEnvironmentPathChangeEvent, Environment, EnvironmentsChangeEvent, ProposedExtensionAPI, @@ -17,7 +17,7 @@ import { Resource, EnvironmentType, EnvironmentTools, - EnvironmentId, + EnvironmentPath, } from './proposedApiTypes'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; @@ -38,7 +38,7 @@ type ActiveEnvironmentChangeEvent = { path: string; }; -const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const onDidActiveInterpreterChangedEvent = new EventEmitter(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); @@ -164,24 +164,34 @@ export function buildProposedApi( const proposed: ProposedExtensionAPI = { environment: { - getActiveEnvironmentId(resource?: Resource) { - sendApiTelemetry('getActiveEnvironmentId'); + getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentPath'); resource = resource && 'uri' in resource ? resource.uri : resource; const path = configService.getSettings(resource).pythonPath; const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); - return { id, path }; + return { + id, + path, + /** + * @deprecated Only provided for backwards compatibility and will soon be removed. + */ + pathType: 'interpreterPath', + }; }, - updateActiveEnvironmentId(env: Environment | EnvironmentId | string, resource?: Resource): Promise { - sendApiTelemetry('updateActiveEnvironmentId'); + updateActiveEnvironmentPath( + env: Environment | EnvironmentPath | string, + resource?: Resource, + ): Promise { + sendApiTelemetry('updateActiveEnvironmentPath'); const path = typeof env !== 'string' ? env.path : env; resource = resource && 'uri' in resource ? resource.uri : resource; return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - get onDidChangeActiveEnvironmentId() { - sendApiTelemetry('onDidChangeActiveEnvironmentId'); + get onDidChangeActiveEnvironmentPath() { + sendApiTelemetry('onDidChangeActiveEnvironmentPath'); return onDidActiveInterpreterChangedEvent.event; }, - resolveEnvironment: async (env: Environment | EnvironmentId | string) => { + resolveEnvironment: async (env: Environment | EnvironmentPath | string) => { let path = typeof env !== 'string' ? env.path : env; if (pathUtils.basename(path) === path) { // Value can be `python`, `python3`, `python3.9` etc. diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 89f9f1e4e5f4..ee6e8e3384d7 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -12,7 +12,7 @@ export interface ProposedExtensionAPI { * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root * scenario. If `undefined`, then the API returns what ever is set for the workspace. */ - getActiveEnvironmentId(resource?: Resource): EnvironmentId; + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; /** * Sets the active environment path for the python extension for the resource. Configuration target will always * be the workspace folder. @@ -20,14 +20,14 @@ export interface ProposedExtensionAPI { * the environment itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ - updateActiveEnvironmentId( - environment: Environment | EnvironmentId | string, + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, resource?: Resource, ): Promise; /** * This event is triggered when the active environment setting changes. */ - readonly onDidChangeActiveEnvironmentId: Event; + readonly onDidChangeActiveEnvironmentPath: Event; /** * Carries environments found by the extension at the time of fetching the property. Note this may not * contain all environments in the system as a refresh might be going on. @@ -55,7 +55,9 @@ export interface ProposedExtensionAPI { * @param environment : Full path to environment folder or python executable for the environment. Can also pass * the environment id or the environment itself. */ - resolveEnvironment(environment: Environment | EnvironmentId | string): Promise; + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; }; } @@ -70,7 +72,7 @@ export type RefreshOptions = { /** * Details about the environment. Note the environment folder, type and name never changes over time. */ -export type Environment = EnvironmentId & { +export type Environment = EnvironmentPath & { /** * Carries details about python executable. */ @@ -175,7 +177,7 @@ export type EnvironmentsChangeEvent = { readonly type: 'add' | 'remove' | 'update'; }; -export type ActiveEnvironmentIdChangeEvent = EnvironmentId & { +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { /** * Workspace folder the environment changed for. */ @@ -187,7 +189,7 @@ export type ActiveEnvironmentIdChangeEvent = EnvironmentId & { */ export type Resource = Uri | WorkspaceFolder; -export type EnvironmentId = { +export type EnvironmentPath = { /** * The ID of the environment. */ diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 816bf1051d25..2bee65e70f65 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -26,12 +26,13 @@ import { sleep } from './core'; import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; import { Architecture } from '../client/common/utils/platform'; import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/base/watcher'; +import { normCasePath } from '../client/common/platform/fs-paths'; import { - ProposedExtensionAPI, - ActiveEnvironmentIdChangeEvent, + ActiveEnvironmentPathChangeEvent, + EnvironmentPath, EnvironmentsChangeEvent, + ProposedExtensionAPI, } from '../client/proposedApiTypes'; -import { normCasePath } from '../client/common/platform/fs-paths'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; @@ -74,8 +75,8 @@ suite('Proposed Extension API', () => { }); test('Provide an event to track when active environment details change', async () => { - const events: ActiveEnvironmentIdChangeEvent[] = []; - proposed.environment.onDidChangeActiveEnvironmentId((e) => { + const events: ActiveEnvironmentPathChangeEvent[] = []; + proposed.environment.onDidChangeActiveEnvironmentPath((e) => { events.push(e); }); reportActiveInterpreterChanged({ path: 'path/to/environment', resource: undefined }); @@ -85,32 +86,44 @@ suite('Proposed Extension API', () => { ]); }); - test('getActiveEnvironmentId: No resource', () => { + test('getActiveEnvironmentPath: No resource', () => { const pythonPath = 'this/is/a/test/path'; configService .setup((c) => c.getSettings(undefined)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); - const actual = proposed.environment.getActiveEnvironmentId(); - assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath }); + const actual = proposed.environment.getActiveEnvironmentPath(); + assert.deepEqual(actual, ({ + id: normCasePath(pythonPath), + path: pythonPath, + pathType: 'interpreterPath', + } as unknown) as EnvironmentPath); }); - test('getActiveEnvironmentId: default python', () => { + test('getActiveEnvironmentPath: default python', () => { const pythonPath = 'python'; configService .setup((c) => c.getSettings(undefined)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); - const actual = proposed.environment.getActiveEnvironmentId(); - assert.deepEqual(actual, { id: 'DEFAULT_PYTHON', path: pythonPath }); + const actual = proposed.environment.getActiveEnvironmentPath(); + assert.deepEqual(actual, ({ + id: 'DEFAULT_PYTHON', + path: pythonPath, + pathType: 'interpreterPath', + } as unknown) as EnvironmentPath); }); - test('getActiveEnvironmentId: With resource', () => { + test('getActiveEnvironmentPath: With resource', () => { const pythonPath = 'this/is/a/test/path'; const resource = Uri.file(__filename); configService .setup((c) => c.getSettings(resource)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); - const actual = proposed.environment.getActiveEnvironmentId(resource); - assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath }); + const actual = proposed.environment.getActiveEnvironmentPath(resource); + assert.deepEqual(actual, ({ + id: normCasePath(pythonPath), + path: pythonPath, + pathType: 'interpreterPath', + } as unknown) as EnvironmentPath); }); test('resolveEnvironment: invalid environment (when passed as string)', async () => { @@ -317,24 +330,24 @@ suite('Proposed Extension API', () => { assert.deepEqual(eventValues, expectedEvents); }); - test('updateActiveEnvironmentId: no resource', async () => { + test('updateActiveEnvironmentPath: no resource', async () => { interpreterPathService .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path'); + await proposed.environment.updateActiveEnvironmentPath('this/is/a/test/python/path'); interpreterPathService.verifyAll(); }); - test('updateActiveEnvironmentId: passed as Environment', async () => { + test('updateActiveEnvironmentPath: passed as Environment', async () => { interpreterPathService .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironmentId({ + await proposed.environment.updateActiveEnvironmentPath({ id: normCasePath('this/is/a/test/python/path'), path: 'this/is/a/test/python/path', }); @@ -342,19 +355,19 @@ suite('Proposed Extension API', () => { interpreterPathService.verifyAll(); }); - test('updateActiveEnvironmentId: with uri', async () => { + test('updateActiveEnvironmentPath: with uri', async () => { const uri = Uri.parse('a'); interpreterPathService .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path', uri); + await proposed.environment.updateActiveEnvironmentPath('this/is/a/test/python/path', uri); interpreterPathService.verifyAll(); }); - test('updateActiveEnvironmentId: with workspace folder', async () => { + test('updateActiveEnvironmentPath: with workspace folder', async () => { const uri = Uri.parse('a'); interpreterPathService .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) @@ -366,7 +379,7 @@ suite('Proposed Extension API', () => { index: 0, }; - await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path', workspace); + await proposed.environment.updateActiveEnvironmentPath('this/is/a/test/python/path', workspace); interpreterPathService.verifyAll(); }); From 743bb5cbf81e45c967c1ee539d1edf59075c15fc Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 27 Sep 2022 11:28:41 -0700 Subject: [PATCH 46/59] Remove API telemetry containing extension display name (#19895) Just the extension ID is sufficient. --- src/client/deprecatedProposedApi.ts | 9 +++++---- src/client/proposedApi.ts | 10 +++++----- src/client/telemetry/index.ts | 6 +----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index ef4d8a1d5eaa..c178c1f9f0a7 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -13,6 +13,7 @@ import { } from './deprecatedProposedApiTypes'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; +import { traceVerbose } from './logging'; import { PythonEnvInfo } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; @@ -68,13 +69,13 @@ export function buildDeprecatedProposedApi( } extensions .determineExtensionFromCallStack() - .then((info) => + .then((info) => { sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { apiName, extensionId: info.extensionId, - displayName: info.displayName, - }), - ) + }); + traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + }) .ignoreErrors(); } diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 1147e839d0a5..543c1dfb426a 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -23,7 +23,7 @@ import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironment import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; -import { traceError } from './logging'; +import { traceError, traceVerbose } from './logging'; import { normCasePath } from './common/platform/fs-paths'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; @@ -108,13 +108,13 @@ export function buildProposedApi( function sendApiTelemetry(apiName: string) { extensions .determineExtensionFromCallStack() - .then((info) => + .then((info) => { sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { apiName, extensionId: info.extensionId, - displayName: info.displayName, - }), - ) + }); + traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + }) .ignoreErrors(); } disposables.push( diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7211aa38e54f..1d1b2e076c13 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1115,7 +1115,7 @@ export interface IEventNamePropertyMapping { /* __GDPR__ "python_environments_api" : { "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false , "owner": "karrtikr"}, - "displayName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false, "owner": "karrtikr" } + "apiName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false, "owner": "karrtikr" } } */ [EventName.PYTHON_ENVIRONMENTS_API]: { @@ -1123,10 +1123,6 @@ export interface IEventNamePropertyMapping { * The ID of the extension calling the API. */ extensionId: string; - /** - * The name of the extension as displayed in marketplace. - */ - displayName: string; /** * The name of the API called. */ From 870ae8f7553d34b0e8157f73283c705e16a65ff7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 27 Sep 2022 11:32:50 -0700 Subject: [PATCH 47/59] Allow to specify title and placeholder in API for interpreter quickpick (#19896) For https://github.com/microsoft/vscode-python/issues/19891 --- src/client/common/utils/multiStepInput.ts | 2 +- .../commands/setInterpreter.ts | 23 +++++++++++++------ src/client/interpreter/configuration/types.ts | 10 ++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index daac8574227d..12aafe3c8099 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -47,7 +47,7 @@ export interface IQuickPickParameters { canGoBack?: boolean; items: T[]; activeItem?: T | Promise; - placeholder: string; + placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; matchOnDetail?: boolean; diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 0c04da6e3b3f..1d7f3b910f52 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -128,6 +128,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem input: IMultiStepInput, state: InterpreterStateArgs, filter?: (i: PythonEnvironment) => boolean, + params?: { placeholder?: string | null; title?: string | null }, ): Promise> { // If the list is refreshing, it's crucial to maintain sorting order at all // times so that the visible items do not change. @@ -138,19 +139,26 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem this.configurationService.getSettings(state.workspace).pythonPath, state.workspace ? state.workspace.fsPath : undefined, ); + const placeholder = + params?.placeholder === null + ? undefined + : params?.placeholder ?? + localize( + 'InterpreterQuickPickList.quickPickListPlaceholder', + 'Selected Interpreter: {0}', + currentInterpreterPathDisplay, + ); + const title = + params?.title === null ? undefined : params?.title ?? InterpreterQuickPickList.browsePath.openButtonLabel; const selection = await input.showQuickPick>({ - placeholder: localize( - 'InterpreterQuickPickList.quickPickListPlaceholder', - 'Selected Interpreter: {0}', - currentInterpreterPathDisplay, - ), + placeholder, items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, - title: InterpreterQuickPickList.browsePath.openButtonLabel, + title, customButtonSetups: [ { button: this.refreshButton, @@ -503,10 +511,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem public async getInterpreterViaQuickPick( workspace: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: { placeholder?: string | null; title?: string | null }, ): Promise { const interpreterState: InterpreterStateArgs = { path: undefined, workspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this._pickInterpreter(input, s, filter), interpreterState); + await multiStep.run((input, s) => this._pickInterpreter(input, s, filter, params), interpreterState); return interpreterState.path; } diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 1e57f4ffcfec..264ac523538d 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -71,5 +71,15 @@ export interface IInterpreterQuickPick { getInterpreterViaQuickPick( workspace: Resource, filter?: (i: PythonEnvironment) => boolean, + params?: { + /** + * Specify `null` if a placeholder is not required. + */ + placeholder?: string | null; + /** + * Specify `null` if a title is not required. + */ + title?: string | null; + }, ): Promise; } From a4f08a8564fe45b0e86d3a1d8dc5c597ea8695b7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 27 Sep 2022 14:54:27 -0700 Subject: [PATCH 48/59] Temporarily expose `PYTHONPATH` for Pylance (#19899) Done temporarily on request of Pylance so they can begin testing. --- src/client/api.ts | 21 ++++++++++++++++++++- src/test/api.functional.test.ts | 6 ++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/client/api.ts b/src/client/api.ts index 366515da3e83..78663feab712 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -4,9 +4,11 @@ 'use strict'; import { noop } from 'lodash'; +import { Uri, Event } from 'vscode'; import { IExtensionApi } from './apiTypes'; import { isTestExecution } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; +import { IEnvironmentVariablesProvider } from './common/variables/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; @@ -22,7 +24,17 @@ export function buildApi( const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); - const api: IExtensionApi = { + const envService = serviceContainer.get(IEnvironmentVariablesProvider); + const api: IExtensionApi & { + /** + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. + */ + pylance: { + getPythonPathVar: (resource?: Uri) => Promise; + readonly onDidEnvironmentVariablesChange: Event; + }; + } = { // 'ready' will propagate the exception, but we must log it here first. ready: ready.catch((ex) => { traceError('Failure during activation.', ex); @@ -65,6 +77,13 @@ export function buildApi( ? jupyterIntegration.showDataViewer.bind(jupyterIntegration) : (noop as any), }, + pylance: { + getPythonPathVar: async (resource?: Uri) => { + const envs = await envService.getEnvironmentVariables(resource); + return envs.PYTHONPATH; + }, + onDidEnvironmentVariablesChange: envService.onDidEnvironmentVariablesChange, + }, }; // In test environment return the DI Container. diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts index 08b0281b4d54..490b5d86b8b3 100644 --- a/src/test/api.functional.test.ts +++ b/src/test/api.functional.test.ts @@ -12,6 +12,7 @@ import { buildApi } from '../client/api'; import { ConfigurationService } from '../client/common/configuration/service'; import { EXTENSION_ROOT_DIR } from '../client/common/constants'; import { IConfigurationService } from '../client/common/types'; +import { IEnvironmentVariablesProvider } from '../client/common/variables/types'; import { IInterpreterService } from '../client/interpreter/contracts'; import { InterpreterService } from '../client/interpreter/interpreterService'; import { ServiceContainer } from '../client/ioc/container'; @@ -27,16 +28,21 @@ suite('Extension API', () => { let serviceManager: IServiceManager; let configurationService: IConfigurationService; let interpreterService: IInterpreterService; + let environmentVariablesProvider: IEnvironmentVariablesProvider; setup(() => { serviceContainer = mock(ServiceContainer); serviceManager = mock(ServiceManager); configurationService = mock(ConfigurationService); interpreterService = mock(InterpreterService); + environmentVariablesProvider = mock(); when(serviceContainer.get(IConfigurationService)).thenReturn( instance(configurationService), ); + when(serviceContainer.get(IEnvironmentVariablesProvider)).thenReturn( + instance(environmentVariablesProvider), + ); when(serviceContainer.get(IInterpreterService)).thenReturn(instance(interpreterService)); }); From a500e16ad374e5d0502e7cdf77ace0af819e2171 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 27 Sep 2022 16:10:33 -0700 Subject: [PATCH 49/59] Only log deprecation warning once for each extension (#19901) For https://github.com/microsoft/vscode-python/issues/19900 --- src/client/deprecatedProposedApi.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index c178c1f9f0a7..459f2b1bf529 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -63,10 +63,8 @@ export function buildDeprecatedProposedApi( const interpreterPathService = serviceContainer.get(IInterpreterPathService); const interpreterService = serviceContainer.get(IInterpreterService); const extensions = serviceContainer.get(IExtensions); + const warningLogged = new Set(); function sendApiTelemetry(apiName: string, warnLog = true) { - if (warnLog) { - console.warn('Extension is using deprecated python APIs which will be removed soon'); - } extensions .determineExtensionFromCallStack() .then((info) => { @@ -75,6 +73,12 @@ export function buildDeprecatedProposedApi( extensionId: info.extensionId, }); traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + if (warnLog && !warningLogged.has(info.extensionId)) { + console.warn( + `${info.extensionId} extension is using deprecated python APIs which will be removed soon.`, + ); + warningLogged.add(info.extensionId); + } }) .ignoreErrors(); } From f9f3b96aece1a4916302defd8d57a1e15dade5c9 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 29 Sep 2022 13:53:17 -0700 Subject: [PATCH 50/59] Add createEnvironment to activation trigger. (#19915) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0f9f3e97cb2a..51754e2f9fb2 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "onCommand:python.enableSourceMapSupport", "onCommand:python.launchTensorBoard", "onCommand:python.clearCacheAndReload", + "onCommand:python.createEnvironment", "onWalkthrough:pythonWelcome", "onWalkthrough:pythonWelcomeWithDS", "onWalkthrough:pythonDataScienceWelcome", From 6fe19ed6f2c4da580bab119c694f2c627204cd24 Mon Sep 17 00:00:00 2001 From: Heejae Chang <1333179+heejaechang@users.noreply.github.com> Date: Thu, 29 Sep 2022 14:49:19 -0700 Subject: [PATCH 51/59] Removed ILanguageServer no longer used. (#19921) I am removing ILanguageServer and all interfaces/types existed around it since it seems it is no longer used. ... it looks like ILanguageServer used to be used for old jupyter (https://github.com/microsoft/vscode-python/blob/main/src/client/jupyter/jupyterIntegration.ts#L146) but it no longer used by jupyter anymore (https://github.com/microsoft/vscode-jupyter/blob/70dc27d55c95cb81bfd32a6505b7fb357bfe624e/src/platform/api/types.ts#L74). this PR (https://github.com/microsoft/vscode-jupyter/pull/10889) removed it from jupyter since it is not used. but it is never removed from python extension. ... I am doing this as a part of on-going client move work since we do not want to expose ILanguageServer to pass to jupyter extension unless it is required. --- .../activation/jedi/languageServerProxy.ts | 2 +- src/client/activation/jedi/manager.ts | 4 - src/client/activation/node/manager.ts | 9 +- src/client/activation/serviceRegistry.ts | 2 - src/client/activation/types.ts | 48 +-- src/client/jupyter/jupyterIntegration.ts | 31 +- .../languageServer/jediLSExtensionManager.ts | 10 +- .../languageServerCapabilities.ts | 307 ------------------ .../languageServer/noneLSExtensionManager.ts | 95 +----- .../pylanceLSExtensionManager.ts | 10 +- src/client/languageServer/types.ts | 11 +- src/client/languageServer/watcher.ts | 19 +- ...eractiveWindowMiddlewareAddon.unit.test.ts | 2 - .../activation/serviceRegistry.unit.test.ts | 4 - .../jediLSExtensionManager.unit.test.ts | 1 - .../languageServerCapabilities.unit.test.ts | 104 ------ .../noneLSExtensionManager.unit.test.ts | 4 - .../pylanceLSExtensionManager.unit.test.ts | 1 - 18 files changed, 21 insertions(+), 643 deletions(-) delete mode 100644 src/client/languageServer/languageServerCapabilities.ts delete mode 100644 src/test/languageServer/languageServerCapabilities.unit.test.ts diff --git a/src/client/activation/jedi/languageServerProxy.ts b/src/client/activation/jedi/languageServerProxy.ts index 94401bc83874..a03b7ba96b39 100644 --- a/src/client/activation/jedi/languageServerProxy.ts +++ b/src/client/activation/jedi/languageServerProxy.ts @@ -16,7 +16,7 @@ import { killPid } from '../../common/process/rawProcessApis'; import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; export class JediLanguageServerProxy implements ILanguageServerProxy { - public languageClient: LanguageClient | undefined; + private languageClient: LanguageClient | undefined; private readonly disposables: Disposable[] = []; diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts index fa3fbc8b7505..672e9a1b33fd 100644 --- a/src/client/activation/jedi/manager.ts +++ b/src/client/activation/jedi/manager.ts @@ -59,10 +59,6 @@ export class JediLanguageServerManager implements ILanguageServerManager { this.disposables.forEach((d) => d.dispose()); } - public get languageProxy(): ILanguageServerProxy | undefined { - return this.languageServerProxy; - } - @traceDecoratorError('Failed to start language server') public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { this.resource = resource; diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index d9b1b48fe754..85d57622e327 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -11,9 +11,10 @@ import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { Commands } from '../commands'; import { NodeLanguageClientMiddleware } from './languageClientMiddleware'; -import { ILanguageServerAnalysisOptions, ILanguageServerManager, ILanguageServerProxy } from '../types'; +import { ILanguageServerAnalysisOptions, ILanguageServerManager } from '../types'; import { traceDecoratorError, traceDecoratorVerbose } from '../../logging'; import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { NodeLanguageServerProxy } from './languageServerProxy'; export class NodeLanguageServerManager implements ILanguageServerManager { private resource!: Resource; @@ -35,7 +36,7 @@ export class NodeLanguageServerManager implements ILanguageServerManager { constructor( private readonly serviceContainer: IServiceContainer, private readonly analysisOptions: ILanguageServerAnalysisOptions, - private readonly languageServerProxy: ILanguageServerProxy, + private readonly languageServerProxy: NodeLanguageServerProxy, commandManager: ICommandManager, private readonly extensions: IExtensions, ) { @@ -59,10 +60,6 @@ export class NodeLanguageServerManager implements ILanguageServerManager { this.disposables.forEach((d) => d.dispose()); } - public get languageProxy(): ILanguageServerProxy { - return this.languageServerProxy; - } - @traceDecoratorError('Failed to start language server') public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { if (this.started) { diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index d7512b533cad..aed2d2e346e4 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -9,7 +9,6 @@ import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, - ILanguageServerCache, ILanguageServerOutputChannel, } from './types'; import { LoadLanguageServerExtension } from './common/loadLanguageServerExtension'; @@ -36,7 +35,6 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); - serviceManager.addBinding(ILanguageServerWatcher, ILanguageServerCache); serviceManager.addSingleton(LspNotebooksExperiment, LspNotebooksExperiment); serviceManager.addBinding(LspNotebooksExperiment, IExtensionSingleActivationService); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 3ca10b29255d..873d608f0bd0 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -3,19 +3,8 @@ 'use strict'; -import { - CodeLensProvider, - CompletionItemProvider, - DefinitionProvider, - DocumentSymbolProvider, - Event, - HoverProvider, - ReferenceProvider, - RenameProvider, - SignatureHelpProvider, -} from 'vscode'; +import { Event } from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; -import * as lsp from 'vscode-languageserver-protocol'; import type { IDisposable, IOutputChannel, Resource } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; @@ -69,41 +58,13 @@ export enum LanguageServerType { None = 'None', } -/** - * This interface is a subset of the vscode-protocol connection interface. - * It's the minimum set of functions needed in order to talk to a language server. - */ -export type ILanguageServerConnection = Pick< - lsp.ProtocolConnection, - 'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest' ->; - -export interface ILanguageServer - extends RenameProvider, - DefinitionProvider, - HoverProvider, - ReferenceProvider, - CompletionItemProvider, - CodeLensProvider, - DocumentSymbolProvider, - SignatureHelpProvider, - IDisposable { - readonly connection?: ILanguageServerConnection; - readonly capabilities?: lsp.ServerCapabilities; -} - export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); -export interface ILanguageServerActivator extends ILanguageServer { +export interface ILanguageServerActivator { start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; activate(): void; deactivate(): void; } -export const ILanguageServerCache = Symbol('ILanguageServerCache'); -export interface ILanguageServerCache { - get(resource: Resource, interpreter?: PythonEnvironment): Promise; -} - export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); export interface ILanguageClientFactory { createLanguageClient( @@ -121,7 +82,6 @@ export interface ILanguageServerAnalysisOptions extends IDisposable { } export const ILanguageServerManager = Symbol('ILanguageServerManager'); export interface ILanguageServerManager extends IDisposable { - readonly languageProxy: ILanguageServerProxy | undefined; start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; connect(): void; disconnect(): void; @@ -129,10 +89,6 @@ export interface ILanguageServerManager extends IDisposable { export const ILanguageServerProxy = Symbol('ILanguageServerProxy'); export interface ILanguageServerProxy extends IDisposable { - /** - * LanguageClient in use - */ - languageClient: LanguageClient | undefined; start( resource: Resource, interpreter: PythonEnvironment | undefined, diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index a99e5e8057d1..692bef74214d 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -6,10 +6,8 @@ import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; -import { CancellationToken, Disposable, Event, Extension, Memento, Uri } from 'vscode'; -import * as lsp from 'vscode-languageserver-protocol'; +import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode'; import type { SemVer } from 'semver'; -import { ILanguageServerCache, ILanguageServerConnection } from '../activation/types'; import { IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID } from '../common/constants'; import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; @@ -23,7 +21,6 @@ import { ProductInstallStatus, Resource, } from '../common/types'; -import { isResource } from '../common/utils/misc'; import { getDebugpyPackagePath } from '../debugger/extension/adapter/remoteLaunchers'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; import { IInterpreterQuickPickItem, IInterpreterSelector } from '../interpreter/configuration/types'; @@ -38,11 +35,6 @@ import { import { PythonEnvironment } from '../pythonEnvironments/info'; import { IDataViewerDataProvider, IJupyterUriProvider } from './types'; -interface ILanguageServer extends Disposable { - readonly connection: ILanguageServerConnection; - readonly capabilities: lsp.ServerCapabilities; -} - /** * This allows Python extension to update Product enum without breaking Jupyter. * I.e. we have a strict contract, else using numbers (in enums) is bound to break across products. @@ -139,11 +131,6 @@ type PythonApiForJupyterExtension = { * Retrieve interpreter path selected for Jupyter server from Python memento storage */ getInterpreterPathSelectedForJupyterServer(): string | undefined; - /** - * Returns a ILanguageServer that can be used for communicating with a language server process. - * @param resource file that determines which connection to return - */ - getLanguageServer(resource?: InterpreterUri): Promise; /** * Registers a visibility filter for the interpreter status bar. */ @@ -207,7 +194,6 @@ export class JupyterExtensionIntegration { @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, @inject(IInstaller) private readonly installer: IInstaller, @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, - @inject(ILanguageServerCache) private readonly languageServerCache: ILanguageServerCache, @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, @inject(IComponentAdapter) private pyenvs: IComponentAdapter, @@ -272,21 +258,6 @@ export class JupyterExtensionIntegration { getDebuggerPath: async () => dirname(getDebugpyPackagePath()), getInterpreterPathSelectedForJupyterServer: () => this.globalState.get('INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'), - getLanguageServer: async (r) => { - const resource = isResource(r) ? r : undefined; - const interpreter = !isResource(r) ? r : undefined; - const client = await this.languageServerCache.get(resource, interpreter); - - // Some language servers don't support the connection yet. - if (client && client.connection && client.capabilities) { - return { - connection: client.connection, - capabilities: client.capabilities, - dispose: client.dispose, - }; - } - return undefined; - }, registerInterpreterStatusFilter: this.interpreterDisplay.registerVisibilityFilter.bind( this.interpreterDisplay, ), diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts index a4836523660f..4cbfb6f33466 100644 --- a/src/client/languageServer/jediLSExtensionManager.ts +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -19,14 +19,12 @@ import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { traceError } from '../logging'; import { PythonEnvironment } from '../pythonEnvironments/info'; -import { LanguageServerCapabilities } from './languageServerCapabilities'; import { ILanguageServerExtensionManager } from './types'; -export class JediLSExtensionManager extends LanguageServerCapabilities - implements IDisposable, ILanguageServerExtensionManager { - serverManager: JediLanguageServerManager; +export class JediLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + private serverProxy: JediLanguageServerProxy; - serverProxy: JediLanguageServerProxy; + serverManager: JediLanguageServerManager; clientFactory: JediLanguageClientFactory; @@ -43,8 +41,6 @@ export class JediLSExtensionManager extends LanguageServerCapabilities environmentService: IEnvironmentVariablesProvider, commandManager: ICommandManager, ) { - super(); - this.analysisOptions = new JediLanguageServerAnalysisOptions( environmentService, outputChannel, diff --git a/src/client/languageServer/languageServerCapabilities.ts b/src/client/languageServer/languageServerCapabilities.ts deleted file mode 100644 index e5fc1e3f276e..000000000000 --- a/src/client/languageServer/languageServerCapabilities.ts +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit, -} from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient/node'; -import { ILanguageServer, ILanguageServerConnection, ILanguageServerProxy } from '../activation/types'; -import { ILanguageServerCapabilities } from './types'; - -/* - * The Language Server Capabilities class implements the ILanguageServer interface to provide support for the existing Jupyter integration. - */ -export class LanguageServerCapabilities implements ILanguageServerCapabilities { - serverProxy: ILanguageServerProxy | undefined; - - public dispose(): void { - // Nothing to do here. - } - - get(): Promise { - return Promise.resolve(this); - } - - public get connection(): ILanguageServerConnection | undefined { - const languageClient = this.getLanguageClient(); - if (languageClient) { - // Return an object that looks like a connection - return { - sendNotification: languageClient.sendNotification.bind(languageClient), - sendRequest: languageClient.sendRequest.bind(languageClient), - sendProgress: languageClient.sendProgress.bind(languageClient), - onRequest: languageClient.onRequest.bind(languageClient), - onNotification: languageClient.onNotification.bind(languageClient), - onProgress: languageClient.onProgress.bind(languageClient), - }; - } - - return undefined; - } - - public get capabilities(): vscodeLanguageClient.ServerCapabilities | undefined { - const languageClient = this.getLanguageClient(); - if (languageClient) { - return languageClient.initializeResult?.capabilities; - } - - return undefined; - } - - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideRenameEdits(document, position, newName, token); - } - - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideDefinition(document, position, token); - } - - public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - return this.handleProvideHover(document, position, token); - } - - public provideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideReferences(document, position, context, token); - } - - public provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext, - ): ProviderResult { - return this.handleProvideCompletionItems(document, position, token, context); - } - - public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { - return this.handleProvideCodeLenses(document, token); - } - - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideDocumentSymbols(document, token); - } - - public provideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - context: SignatureHelpContext, - ): ProviderResult { - return this.handleProvideSignatureHelp(document, position, token, context); - } - - protected getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { - return this.serverProxy?.languageClient; - } - - private async handleProvideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.RenameParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - newName, - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.RenameRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asWorkspaceEdit(result); - } - } - - return undefined; - } - - private async handleProvideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.DefinitionRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asDefinitionResult(result); - } - } - - return undefined; - } - - private async handleProvideHover( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.HoverRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asHover(result); - } - } - - return undefined; - } - - private async handleProvideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.ReferenceParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - context, - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.ReferencesRequest.type, args, token); - if (result) { - // Remove undefined part. - return result.map((l) => { - const r = languageClient!.protocol2CodeConverter.asLocation(l); - return r!; - }); - } - } - - return undefined; - } - - private async handleProvideCodeLenses( - document: TextDocument, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.CodeLensParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.CodeLensRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asCodeLenses(result); - } - } - - return undefined; - } - - private async handleProvideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args = languageClient.code2ProtocolConverter.asCompletionParams(document, position, context); - const result = await languageClient.sendRequest(vscodeLanguageClient.CompletionRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asCompletionResult(result); - } - } - - return undefined; - } - - private async handleProvideDocumentSymbols( - document: TextDocument, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.DocumentSymbolParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - }; - const result = await languageClient.sendRequest( - vscodeLanguageClient.DocumentSymbolRequest.type, - args, - token, - ); - if (result && result.length) { - if ((result[0] as DocumentSymbol).range) { - // Document symbols - const docSymbols = result as vscodeLanguageClient.DocumentSymbol[]; - return languageClient.protocol2CodeConverter.asDocumentSymbols(docSymbols); - } - // Document symbols - const symbols = result as vscodeLanguageClient.SymbolInformation[]; - return languageClient.protocol2CodeConverter.asSymbolInformations(symbols); - } - } - - return undefined; - } - - private async handleProvideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - _context: SignatureHelpContext, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - }; - const result = await languageClient.sendRequest( - vscodeLanguageClient.SignatureHelpRequest.type, - args, - token, - ); - if (result) { - return languageClient.protocol2CodeConverter.asSignatureHelp(result); - } - } - - return undefined; - } -} diff --git a/src/client/languageServer/noneLSExtensionManager.ts b/src/client/languageServer/noneLSExtensionManager.ts index b5f6207e0a41..1d93ea50be51 100644 --- a/src/client/languageServer/noneLSExtensionManager.ts +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -3,45 +3,15 @@ /* eslint-disable class-methods-use-this */ -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit, -} from 'vscode'; -import { ILanguageServer, ILanguageServerProxy } from '../activation/types'; import { ILanguageServerExtensionManager } from './types'; // This LS manager implements ILanguageServer directly // instead of extending LanguageServerCapabilities because it doesn't need to do anything. -export class NoneLSExtensionManager implements ILanguageServer, ILanguageServerExtensionManager { - serverProxy: ILanguageServerProxy | undefined; - - constructor() { - this.serverProxy = undefined; - } - +export class NoneLSExtensionManager implements ILanguageServerExtensionManager { dispose(): void { // Nothing to do here. } - get(): Promise { - return Promise.resolve(this); - } - startLanguageServer(): Promise { return Promise.resolve(); } @@ -58,67 +28,4 @@ export class NoneLSExtensionManager implements ILanguageServer, ILanguageServerE // Nothing to do here. return Promise.resolve(); } - - public provideRenameEdits( - _document: TextDocument, - _position: Position, - _newName: string, - _token: CancellationToken, - ): ProviderResult { - return null; - } - - public provideDefinition( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - ): ProviderResult { - return null; - } - - public provideHover( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - ): ProviderResult { - return null; - } - - public provideReferences( - _document: TextDocument, - _position: Position, - _context: ReferenceContext, - _token: CancellationToken, - ): ProviderResult { - return null; - } - - public provideCompletionItems( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - _context: CompletionContext, - ): ProviderResult { - return null; - } - - public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { - return null; - } - - public provideDocumentSymbols( - _document: TextDocument, - _token: CancellationToken, - ): ProviderResult { - return null; - } - - public provideSignatureHelp( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - _context: SignatureHelpContext, - ): ProviderResult { - return null; - } } diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index 2cc74308feea..3865886880b2 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -25,14 +25,12 @@ import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { traceLog } from '../logging'; import { PythonEnvironment } from '../pythonEnvironments/info'; -import { LanguageServerCapabilities } from './languageServerCapabilities'; import { ILanguageServerExtensionManager } from './types'; -export class PylanceLSExtensionManager extends LanguageServerCapabilities - implements IDisposable, ILanguageServerExtensionManager { - serverManager: NodeLanguageServerManager; +export class PylanceLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + private serverProxy: NodeLanguageServerProxy; - serverProxy: NodeLanguageServerProxy; + serverManager: NodeLanguageServerManager; clientFactory: NodeLanguageClientFactory; @@ -53,8 +51,6 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities readonly applicationShell: IApplicationShell, lspNotebooksExperiment: LspNotebooksExperiment, ) { - super(); - this.analysisOptions = new NodeLanguageServerAnalysisOptions( outputChannel, workspaceService, diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index cab882f78f3e..f7cad157fcef 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ILanguageServer, ILanguageServerProxy, LanguageServerType } from '../activation/types'; +import { LanguageServerType } from '../activation/types'; import { Resource } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; @@ -15,12 +15,7 @@ export interface ILanguageServerWatcher { readonly languageServerType: LanguageServerType; startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise; restartLanguageServers(): Promise; -} - -export interface ILanguageServerCapabilities extends ILanguageServer { - serverProxy: ILanguageServerProxy | undefined; - - get(): Promise; + get(resource: Resource, interpreter?: PythonEnvironment): Promise; } /** @@ -28,7 +23,7 @@ export interface ILanguageServerCapabilities extends ILanguageServer { * They are responsible for starting and stopping the language server provided by their LS extension. * They also extend the `ILanguageServer` interface via `ILanguageServerCapabilities` to continue supporting the Jupyter integration. */ -export interface ILanguageServerExtensionManager extends ILanguageServerCapabilities { +export interface ILanguageServerExtensionManager { startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise; stopLanguageServer(): Promise; canStartLanguageServer(interpreter: PythonEnvironment | undefined): boolean; diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 8fb6591b70c8..f7c4193bde90 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -6,13 +6,7 @@ import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; import * as nls from 'vscode-nls'; import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; -import { - IExtensionActivationService, - ILanguageServer, - ILanguageServerCache, - ILanguageServerOutputChannel, - LanguageServerType, -} from '../activation/types'; +import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { IFileSystem } from '../common/platform/types'; import { @@ -41,11 +35,8 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @injectable() /** * The Language Server Watcher class implements the ILanguageServerWatcher interface, which is the one-stop shop for language server activation. - * - * It also implements the ILanguageServerCache interface needed by our Jupyter support. */ -export class LanguageServerWatcher - implements IExtensionActivationService, ILanguageServerWatcher, ILanguageServerCache { +export class LanguageServerWatcher implements IExtensionActivationService, ILanguageServerWatcher { public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; languageServerExtensionManager: ILanguageServerExtensionManager | undefined; @@ -202,9 +193,7 @@ export class LanguageServerWatcher }); } - // ILanguageServerCache - - public async get(resource?: Resource): Promise { + public async get(resource?: Resource): Promise { const key = this.getWorkspaceKey(resource, this.languageServerType); let languageServerExtensionManager = this.workspaceLanguageServers.get(key); @@ -212,7 +201,7 @@ export class LanguageServerWatcher languageServerExtensionManager = await this.startAndGetLanguageServer(this.languageServerType, resource); } - return Promise.resolve(languageServerExtensionManager.get()); + return Promise.resolve(languageServerExtensionManager); } // Private methods diff --git a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts index 9fd78760804b..472dea147503 100644 --- a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts +++ b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts @@ -19,7 +19,6 @@ import { } from '../../../client/interpreter/contracts'; import { IInterpreterSelector } from '../../../client/interpreter/configuration/types'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { ILanguageServerCache } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { MockMemento } from '../../mocks/mementos'; @@ -37,7 +36,6 @@ suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { mock(), mock(), mock(), - mock(), new MockMemento(), mock(), mock(), diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index 1d3f4f383082..cf715b90ecfe 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -9,14 +9,11 @@ import { registerTypes } from '../../client/activation/serviceRegistry'; import { IExtensionActivationManager, IExtensionSingleActivationService, - ILanguageServerCache, ILanguageServerOutputChannel, } from '../../client/activation/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; import { LoadLanguageServerExtension } from '../../client/activation/common/loadLanguageServerExtension'; -import { ILanguageServerWatcher } from '../../client/languageServer/types'; -import { LanguageServerWatcher } from '../../client/languageServer/watcher'; suite('Unit Tests - Language Server Activation Service Registry', () => { let serviceManager: IServiceManager; @@ -28,7 +25,6 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { test('Ensure common services are registered', async () => { registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher)).once(); verify( serviceManager.add(IExtensionActivationManager, ExtensionActivationManager), ).once(); diff --git a/src/test/languageServer/jediLSExtensionManager.unit.test.ts b/src/test/languageServer/jediLSExtensionManager.unit.test.ts index 5fffef66a92e..b57a0bbd096d 100644 --- a/src/test/languageServer/jediLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/jediLSExtensionManager.unit.test.ts @@ -35,7 +35,6 @@ suite('Language Server - Jedi LS extension manager', () => { test('Constructor should create a client proxy, a server manager and a server proxy', () => { assert.notStrictEqual(manager.clientFactory, undefined); assert.notStrictEqual(manager.serverManager, undefined); - assert.notStrictEqual(manager.serverProxy, undefined); }); test('canStartLanguageServer should return true if an interpreter is passed in', () => { diff --git a/src/test/languageServer/languageServerCapabilities.unit.test.ts b/src/test/languageServer/languageServerCapabilities.unit.test.ts deleted file mode 100644 index 4392e2901af8..000000000000 --- a/src/test/languageServer/languageServerCapabilities.unit.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import { ILanguageServerProxy } from '../../client/activation/types'; -import { LanguageServerCapabilities } from '../../client/languageServer/languageServerCapabilities'; - -suite('Language server - capabilities', () => { - test('get() should not return undefined', async () => { - const capabilities = new LanguageServerCapabilities(); - - const result = await capabilities.get(); - - assert.notDeepStrictEqual(result, undefined); - }); - - test('The connection property should return an object if there is a language client', () => { - const serverProxy = ({ - languageClient: { - sendNotification: () => { - /* nothing */ - }, - sendRequest: () => { - /* nothing */ - }, - sendProgress: () => { - /* nothing */ - }, - onRequest: () => { - /* nothing */ - }, - onNotification: () => { - /* nothing */ - }, - onProgress: () => { - /* nothing */ - }, - }, - } as unknown) as ILanguageServerProxy; - - const capabilities = new LanguageServerCapabilities(); - capabilities.serverProxy = serverProxy; - - const result = capabilities.connection; - - assert.notDeepStrictEqual(result, undefined); - assert.strictEqual(typeof result, 'object'); - }); - - test('The connection property should return undefined if there is no language client', () => { - const serverProxy = ({} as unknown) as ILanguageServerProxy; - - const capabilities = new LanguageServerCapabilities(); - capabilities.serverProxy = serverProxy; - - const result = capabilities.connection; - - assert.deepStrictEqual(result, undefined); - }); - - test('capabilities() should return an object if there is an initialized language client', () => { - const serverProxy = ({ - languageClient: { - initializeResult: { - capabilities: {}, - }, - }, - } as unknown) as ILanguageServerProxy; - - const capabilities = new LanguageServerCapabilities(); - capabilities.serverProxy = serverProxy; - - const result = capabilities.capabilities; - - assert.notDeepStrictEqual(result, undefined); - assert.strictEqual(typeof result, 'object'); - }); - - test('capabilities() should return undefined if there is no language client', () => { - const serverProxy = ({} as unknown) as ILanguageServerProxy; - - const capabilities = new LanguageServerCapabilities(); - capabilities.serverProxy = serverProxy; - - const result = capabilities.capabilities; - - assert.deepStrictEqual(result, undefined); - }); - - test('capabilities() should return undefined if the language client is not initialized', () => { - const serverProxy = ({ - languageClient: { - initializeResult: undefined, - }, - } as unknown) as ILanguageServerProxy; - - const capabilities = new LanguageServerCapabilities(); - capabilities.serverProxy = serverProxy; - - const result = capabilities.capabilities; - - assert.deepStrictEqual(result, undefined); - }); -}); diff --git a/src/test/languageServer/noneLSExtensionManager.unit.test.ts b/src/test/languageServer/noneLSExtensionManager.unit.test.ts index f662dc152e69..2f27e420ca48 100644 --- a/src/test/languageServer/noneLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/noneLSExtensionManager.unit.test.ts @@ -11,10 +11,6 @@ suite('Language Server - No LS extension manager', () => { manager = new NoneLSExtensionManager(); }); - test('Constructor should not create a server proxy', () => { - assert.strictEqual(manager.serverProxy, undefined); - }); - test('canStartLanguageServer should return true', () => { const result = manager.canStartLanguageServer(); diff --git a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts index 1a51c93d4783..0118cca0764f 100644 --- a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts @@ -45,7 +45,6 @@ suite('Language Server - Pylance LS extension manager', () => { test('Constructor should create a client proxy, a server manager and a server proxy', () => { assert.notStrictEqual(manager.clientFactory, undefined); assert.notStrictEqual(manager.serverManager, undefined); - assert.notStrictEqual(manager.serverProxy, undefined); }); test('canStartLanguageServer should return true if Pylance is installed', () => { From 53a098f73eb43583ddf40fe4b491230c2a749fe8 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Mon, 3 Oct 2022 08:41:09 -0700 Subject: [PATCH 52/59] Improve keyword hover information displayed when using Jedi (#19926) For https://github.com/microsoft/vscode/issues/159247 (ref: https://github.com/pappasam/jedi-language-server/issues/227#issuecomment-1263913655) --- src/client/activation/jedi/analysisOptions.ts | 7 +++++++ src/test/activation/jedi/jediAnalysisOptions.unit.test.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/client/activation/jedi/analysisOptions.ts b/src/client/activation/jedi/analysisOptions.ts index b1a184c91118..67c9af75937c 100644 --- a/src/client/activation/jedi/analysisOptions.ts +++ b/src/client/activation/jedi/analysisOptions.ts @@ -67,6 +67,13 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt didSave: true, didChange: true, }, + hover: { + disable: { + keyword: { + all: true, + }, + }, + }, workspace: { extraPaths: distinctExtraPaths, symbols: { diff --git a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts index 3899284e9f28..296d31ba2ddb 100644 --- a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts +++ b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts @@ -69,6 +69,7 @@ suite('Jedi LSP - analysis Options', () => { expect(result.initializationOptions.diagnostics.didOpen).to.deep.equal(true); expect(result.initializationOptions.diagnostics.didSave).to.deep.equal(true); expect(result.initializationOptions.diagnostics.didChange).to.deep.equal(true); + expect(result.initializationOptions.hover.disable.keyword.all).to.deep.equal(true); expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([]); expect(result.initializationOptions.workspace.symbols.maxSymbols).to.deep.equal(0); }); From ae6394375a269f340fd7cab1c9449597518f3ab2 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 3 Oct 2022 11:50:12 -0700 Subject: [PATCH 53/59] Fix bugs found during TPI (#19925) Closes https://github.com/microsoft/vscode-python/issues/19889 Closes https://github.com/microsoft/vscode-python/issues/19890 Closes https://github.com/microsoft/vscode-python/issues/19891 Closes https://github.com/microsoft/vscode-python/issues/19894 Closes https://github.com/microsoft/vscode-python/issues/19897 Closes https://github.com/microsoft/vscode-python/issues/19852 Closes https://github.com/microsoft/vscode-python/issues/19851 Closes https://github.com/microsoft/vscode-python/issues/19838 Closes https://github.com/microsoft/vscode-python/issues/19916 Closes https://github.com/microsoft/vscode-python/issues/19917 --- pythonFiles/create_conda.py | 11 +- pythonFiles/create_venv.py | 38 ++-- src/client/common/utils/localize.ts | 38 ++-- src/client/common/vscodeApis/windowApis.ts | 17 ++ src/client/extensionActivation.ts | 15 +- .../virtualEnvs/virtualEnvPrompt.ts | 7 +- .../creation/common/workspaceSelection.ts | 2 +- .../creation/createEnvApi.ts | 27 ++- .../creation/createEnvQuickPick.ts | 54 ----- .../creation/createEnvironment.ts | 127 ++++++++---- .../provider/condaCreationProvider.ts | 134 ++++++++----- .../provider/condaProgressAndTelemetry.ts | 77 +++++++ .../creation/provider/condaUtils.ts | 4 +- .../creation/provider/venvCreationProvider.ts | 150 ++++++++------ .../provider/venvProgressAndTelemetry.ts | 125 ++++++++++++ .../pythonEnvironments/creation/types.ts | 13 +- src/client/telemetry/constants.ts | 7 + src/client/telemetry/index.ts | 105 ++++++++-- .../virtualEnvs/virtualEnvPrompt.unit.test.ts | 24 ++- .../creation/createEnvQuickPick.unit.test.ts | 69 ------- .../creation/createEnvironment.unit.test.ts | 133 ++++++++---- .../condaCreationProvider.unit.test.ts | 54 ++++- .../venvCreationProvider.unit.test.ts | 189 ++++++------------ 23 files changed, 891 insertions(+), 529 deletions(-) delete mode 100644 src/client/pythonEnvironments/creation/createEnvQuickPick.ts create mode 100644 src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts create mode 100644 src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts delete mode 100644 src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts diff --git a/pythonFiles/create_conda.py b/pythonFiles/create_conda.py index 0e48ee6b2286..9a34de47d51f 100644 --- a/pythonFiles/create_conda.py +++ b/pythonFiles/create_conda.py @@ -85,6 +85,7 @@ def install_packages(env_path: str) -> None: ], "CREATE_CONDA.FAILED_INSTALL_YML", ) + print("CREATE_CONDA.INSTALLED_YML") def add_gitignore(name: str) -> None: @@ -100,7 +101,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: argv = [] args = parse_args(argv) - if not conda_env_exists(args.name): + if conda_env_exists(args.name): + env_path = get_conda_env_path(args.name) + print(f"EXISTING_CONDA_ENV:{env_path}") + else: run_process( [ sys.executable, @@ -114,12 +118,11 @@ def main(argv: Optional[Sequence[str]] = None) -> None: ], "CREATE_CONDA.ENV_FAILED_CREATION", ) + env_path = get_conda_env_path(args.name) + print(f"CREATED_CONDA_ENV:{env_path}") if args.git_ignore: add_gitignore(args.name) - env_path = get_conda_env_path(args.name) - print(f"CREATED_CONDA_ENV:{env_path}") - if args.install: install_packages(env_path) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index 4d9b551798e1..1f31abc5cc87 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -51,7 +51,7 @@ def file_exists(path: Union[str, pathlib.PurePath]) -> bool: def venv_exists(name: str) -> bool: - return os.path.exists(CWD / name) + return os.path.exists(CWD / name) and file_exists(get_venv_path(name)) def run_process(args: Sequence[str], error_message: str) -> None: @@ -72,9 +72,6 @@ def get_venv_path(name: str) -> str: def install_packages(venv_path: str) -> None: - if not is_installed("pip"): - raise VenvError("CREATE_VENV.PIP_NOT_FOUND") - requirements = os.fspath(CWD / "requirements.txt") pyproject = os.fspath(CWD / "pyproject.toml") @@ -89,12 +86,14 @@ def install_packages(venv_path: str) -> None: [venv_path, "-m", "pip", "install", "-r", requirements], "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", ) + print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS") elif file_exists(pyproject): print(f"VENV_INSTALLING_PYPROJECT: {pyproject}") run_process( [venv_path, "-m", "pip", "install", "-e", ".[extras]"], "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT", ) + print("CREATE_VENV.PIP_INSTALLED_PYPROJECT") def add_gitignore(name: str) -> None: @@ -110,20 +109,27 @@ def main(argv: Optional[Sequence[str]] = None) -> None: argv = [] args = parse_args(argv) - if is_installed("venv"): - if not venv_exists(args.name): - run_process( - [sys.executable, "-m", "venv", args.name], - "CREATE_VENV.VENV_FAILED_CREATION", - ) - if args.git_ignore: - add_gitignore(args.name) + if not is_installed("venv"): + raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + + if args.install and not is_installed("pip"): + raise VenvError("CREATE_VENV.PIP_NOT_FOUND") + + if venv_exists(args.name): venv_path = get_venv_path(args.name) - print(f"CREATED_VENV:{venv_path}") - if args.install: - install_packages(venv_path) + print(f"EXISTING_VENV:{venv_path}") else: - raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + run_process( + [sys.executable, "-m", "venv", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) + venv_path = get_venv_path(args.name) + print(f"CREATED_VENV:{venv_path}") + if args.git_ignore: + add_gitignore(args.name) + + if args.install: + install_packages(venv_path) if __name__ == "__main__": diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 0ce9d5420c32..e1e2f8d71184 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -552,10 +552,12 @@ export namespace SwitchToDefaultLS { } export namespace CreateEnv { + export const informEnvCreation = localize( + 'createEnv.informEnvCreation', + 'We have selected the following environment:', + ); export const statusTitle = localize('createEnv.statusTitle', 'Creating environment'); export const statusStarting = localize('createEnv.statusStarting', 'Starting...'); - export const statusError = localize('createEnv.statusError', 'Error.'); - export const statusDone = localize('createEnv.statusDone', 'Done.'); export const hasVirtualEnv = localize('createEnv.hasVirtualEnv', 'Workspace folder contains a virtual environment'); @@ -564,21 +566,23 @@ export namespace CreateEnv { 'Please open a directory when creating an environment using venv.', ); - export const pickWorkspaceTitle = localize( - 'createEnv.workspaceQuickPick.title', + export const pickWorkspacePlaceholder = localize( + 'createEnv.workspaceQuickPick.placeholder', 'Select a workspace to create environment', ); - export const providersQuickPickTitle = localize('createEnv.providersQuickPick.title', 'Select an environment type'); + export const providersQuickPickPlaceholder = localize( + 'createEnv.providersQuickPick.placeholder', + 'Select an environment type', + ); export namespace Venv { export const creating = localize('createEnv.venv.creating', 'Creating venv...'); export const created = localize('createEnv.venv.created', 'Environment created...'); export const installingPackages = localize('createEnv.venv.installingPackages', 'Installing packages...'); - export const waitingForPython = localize('createEnv.venv.waitingForPython', 'Waiting on Python selection...'); - export const waitingForWorkspace = localize( - 'createEnv.venv.waitingForWorkspace', - 'Waiting on workspace selection...', + export const errorCreatingEnvironment = localize( + 'createEnv.venv.errorCreatingEnvironment', + 'Error while creating virtual environment.', ); export const selectPythonQuickPickTitle = localize( 'createEnv.venv.basePython.title', @@ -588,6 +592,7 @@ export namespace CreateEnv { 'createEnv.venv.description', 'Creates a `.venv` virtual environment in the current workspace', ); + export const error = localize('createEnv.venv.error', 'Creating virtual environment failed with error.'); } export namespace Conda { @@ -601,20 +606,11 @@ export namespace CreateEnv { 'createEnv.conda.errorCreatingEnvironment', 'Error while creating conda environment.', ); - export const waitingForWorkspace = localize( - 'createEnv.conda.waitingForWorkspace', - 'Waiting on workspace selection...', - ); - export const waitingForPython = localize( - 'createEnv.conda.waitingForPython', - 'Waiting on Python version selection...', - ); - export const selectPythonQuickPickTitle = localize( - 'createEnv.conda.pythonSelection.title', + export const selectPythonQuickPickPlaceholder = localize( + 'createEnv.conda.pythonSelection.placeholder', 'Please select the version of Python to install in the environment', ); - export const searching = localize('createEnv.conda.searching', 'Searching for conda (base)...'); - export const creating = localize('createEnv.venv.creating', 'Running conda create...'); + export const creating = localize('createEnv.conda.creating', 'Creating conda environment...'); export const providerDescription = localize( 'createEnv.conda.description', 'Creates a `.conda` Conda environment in the current workspace', diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 7def82abc752..07a4b4c4acc6 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -38,6 +38,23 @@ export function showErrorMessage(message: string, ...items: any[]): Thenable< return window.showErrorMessage(message, ...items); } +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showInformationMessage(message: string, ...items: any[]): Thenable { + return window.showInformationMessage(message, ...items); +} + export function withProgress( options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 3d2e026d7da4..d479fe49f944 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -21,7 +21,13 @@ import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './c import { Commands, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IExtensions, IOutputChannel } from './common/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExtensions, + IInterpreterPathService, + IOutputChannel, +} from './common/types'; import { noop } from './common/utils/misc'; import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; @@ -97,11 +103,14 @@ export async function activateComponents( return Promise.all([legacyActivationResult, ...promises]); } -export function activateFeatures(ext: ExtensionState, components: Components): void { +export function activateFeatures(ext: ExtensionState, _components: Components): void { const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( IInterpreterQuickPick, ); - registerCreateEnvironmentFeatures(ext.disposables, components.pythonEnvs, interpreterQuickPick); + const interpreterPathService: IInterpreterPathService = ext.legacyIOC.serviceContainer.get( + IInterpreterPathService, + ); + registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService); } /// ////////////////////////// diff --git a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts index 889e0205747b..7ed18c0e8b2a 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -6,9 +6,9 @@ import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell } from '../../common/application/types'; import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; -import { sleep } from '../../common/utils/async'; import { Common, Interpreters } from '../../common/utils/localize'; import { traceDecoratorError, traceVerbose } from '../../logging'; +import { isCreatingEnvironment } from '../../pythonEnvironments/creation/createEnvApi'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -38,8 +38,9 @@ export class VirtualEnvironmentPrompt implements IExtensionActivationService { @traceDecoratorError('Error in event handler for detection of new environment') protected async handleNewEnvironment(resource: Uri): Promise { - // Wait for a while, to ensure environment gets created and is accessible (as this is slow on Windows) - await sleep(1000); + if (isCreatingEnvironment()) { + return; + } const interpreters = await this.pyenvs.getWorkspaceVirtualEnvInterpreters(resource); const interpreter = Array.isArray(interpreters) && interpreters.length > 0 diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index 6aaee99e1f36..b2dc97882e23 100644 --- a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -51,7 +51,7 @@ export async function pickWorkspaceFolder( const selected = await showQuickPick( getWorkspacesForQuickPick(workspaces), { - title: CreateEnv.pickWorkspaceTitle, + placeHolder: CreateEnv.pickWorkspacePlaceholder, ignoreFocusOut: true, canPickMany: options?.allowMultiSelect, }, diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index 2546a858ced9..76263dd9315a 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -1,16 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable } from 'vscode'; +import { ConfigurationTarget, Disposable } from 'vscode'; import { Commands } from '../../common/constants'; -import { IDisposableRegistry } from '../../common/types'; +import { IDisposableRegistry, IInterpreterPathService } from '../../common/types'; import { registerCommand } from '../../common/vscodeApis/commandApis'; import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; -import { IDiscoveryAPI } from '../base/locator'; -import { handleCreateEnvironmentCommand } from './createEnvQuickPick'; +import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; import { VenvCreationProvider } from './provider/venvCreationProvider'; -import { CreateEnvironmentOptions, CreateEnvironmentProvider } from './types'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; class CreateEnvironmentProviders { private _createEnvProviders: CreateEnvironmentProvider[] = []; @@ -41,20 +42,30 @@ export function registerCreateEnvironmentProvider(provider: CreateEnvironmentPro }); } +export const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreatingEnvironment } = getCreationEvents(); + export function registerCreateEnvironmentFeatures( disposables: IDisposableRegistry, - discoveryApi: IDiscoveryAPI, interpreterQuickPick: IInterpreterQuickPick, + interpreterPathService: IInterpreterPathService, ): void { disposables.push( registerCommand( Commands.Create_Environment, - (options?: CreateEnvironmentOptions): Promise => { + (options?: CreateEnvironmentOptions): Promise => { const providers = _createEnvironmentProviders.getAll(); return handleCreateEnvironmentCommand(providers, options); }, ), ); - disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(discoveryApi, interpreterQuickPick))); + disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick))); disposables.push(registerCreateEnvironmentProvider(condaCreationProvider())); + disposables.push( + onCreateEnvironmentExited(async (e: CreateEnvironmentResult | undefined) => { + if (e && e.path) { + await interpreterPathService.update(e.uri, ConfigurationTarget.WorkspaceFolder, e.path); + showInformationMessage(`${CreateEnv.informEnvCreation} ${e.path}`); + } + }), + ); } diff --git a/src/client/pythonEnvironments/creation/createEnvQuickPick.ts b/src/client/pythonEnvironments/creation/createEnvQuickPick.ts deleted file mode 100644 index de71aa84cd06..000000000000 --- a/src/client/pythonEnvironments/creation/createEnvQuickPick.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License - -import { QuickPickItem } from 'vscode'; -import { CreateEnv } from '../../common/utils/localize'; -import { showQuickPick } from '../../common/vscodeApis/windowApis'; -import { traceError } from '../../logging'; -import { createEnvironment } from './createEnvironment'; -import { CreateEnvironmentOptions, CreateEnvironmentProvider } from './types'; - -interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { - id: string; -} - -async function showCreateEnvironmentQuickPick( - providers: readonly CreateEnvironmentProvider[], -): Promise { - const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ - label: p.name, - description: p.description, - id: p.id, - })); - const selected = await showQuickPick(items, { - title: CreateEnv.providersQuickPickTitle, - matchOnDescription: true, - ignoreFocusOut: true, - }); - - if (selected) { - const selections = providers.filter((p) => p.id === selected.id); - if (selections.length > 0) { - return selections[0]; - } - } - return undefined; -} - -export async function handleCreateEnvironmentCommand( - providers: readonly CreateEnvironmentProvider[], - options?: CreateEnvironmentOptions, -): Promise { - if (providers.length === 1) { - return createEnvironment(providers[0], options); - } - if (providers.length > 1) { - const provider = await showCreateEnvironmentQuickPick(providers); - if (provider) { - return createEnvironment(provider, options); - } - } else { - traceError('No Environment Creation providers were registered.'); - } - return undefined; -} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index a07555273604..7489da89f123 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -1,48 +1,101 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import { CancellationToken, ProgressLocation } from 'vscode'; -import { withProgress } from '../../common/vscodeApis/windowApis'; +import { Event, EventEmitter, QuickPickItem } from 'vscode'; +import { CreateEnv } from '../../common/utils/localize'; +import { showQuickPick } from '../../common/vscodeApis/windowApis'; import { traceError } from '../../logging'; -import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from './types'; -import { Common, CreateEnv } from '../../common/utils/localize'; -import { Commands } from '../../common/constants'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; -export async function createEnvironment( +const onCreateEnvironmentStartedEvent = new EventEmitter(); +const onCreateEnvironmentExitedEvent = new EventEmitter(); + +let startedEventCount = 0; + +function isBusyCreatingEnvironment(): boolean { + return startedEventCount > 0; +} + +function fireStartedEvent(): void { + onCreateEnvironmentStartedEvent.fire(); + startedEventCount += 1; +} + +function fireExitedEvent(result: CreateEnvironmentResult | undefined): void { + onCreateEnvironmentExitedEvent.fire(result); + startedEventCount -= 1; +} + +export function getCreationEvents(): { + onCreateEnvironmentStarted: Event; + onCreateEnvironmentExited: Event; + isCreatingEnvironment: () => boolean; +} { + return { + onCreateEnvironmentStarted: onCreateEnvironmentStartedEvent.event, + onCreateEnvironmentExited: onCreateEnvironmentExitedEvent.event, + isCreatingEnvironment: isBusyCreatingEnvironment, + }; +} + +async function createEnvironment( provider: CreateEnvironmentProvider, options: CreateEnvironmentOptions = { ignoreSourceControl: true, installPackages: true, }, -): Promise { - return withProgress( - { - location: ProgressLocation.Notification, - title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, - cancellable: true, - }, - async (progress: CreateEnvironmentProgress, token: CancellationToken) => { - let hasError = false; - progress.report({ - message: CreateEnv.statusStarting, - }); - try { - const result = await provider.createEnvironment(options, progress, token); - return result; - } catch (ex) { - traceError(ex); - hasError = true; - progress.report({ - message: CreateEnv.statusError, - }); - throw ex; - } finally { - if (!hasError) { - progress.report({ - message: CreateEnv.statusDone, - }); - } - } - }, - ); +): Promise { + let result: CreateEnvironmentResult | undefined; + try { + fireStartedEvent(); + result = await provider.createEnvironment(options); + } finally { + fireExitedEvent(result); + } + return result; +} + +interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { + id: string; +} + +async function showCreateEnvironmentQuickPick( + providers: readonly CreateEnvironmentProvider[], +): Promise { + const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ + label: p.name, + description: p.description, + id: p.id, + })); + const selected = await showQuickPick(items, { + placeHolder: CreateEnv.providersQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selected) { + const selections = providers.filter((p) => p.id === selected.id); + if (selections.length > 0) { + return selections[0]; + } + } + return undefined; +} + +export async function handleCreateEnvironmentCommand( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions, +): Promise { + if (providers.length === 1) { + return createEnvironment(providers[0], options); + } + if (providers.length > 1) { + const provider = await showCreateEnvironmentQuickPick(providers); + if (provider) { + return createEnvironment(provider, options); + } + } else { + traceError('No Environment Creation providers were registered.'); + } + return undefined; } diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 38da9038c59a..3845a9ce8dad 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -1,22 +1,32 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, WorkspaceFolder } from 'vscode'; +import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; -import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { traceError, traceLog } from '../../../logging'; -import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from '../types'; +import { + CreateEnvironmentOptions, + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; -import { CreateEnv } from '../../../common/utils/localize'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { getConda, pickPythonVersion } from './condaUtils'; import { showErrorMessageWithLogs } from '../common/commonUtils'; - -export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; -export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; +import { withProgress } from '../../../common/vscodeApis/windowApis'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { + CondaProgressAndTelemetry, + CONDA_ENV_CREATED_MARKER, + CONDA_ENV_EXISTING_MARKER, +} from './condaProgressAndTelemetry'; function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -44,13 +54,33 @@ function generateCommandArgs(version?: string, options?: CreateEnvironmentOption return command; } +function getCondaEnvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER) || s.startsWith(CONDA_ENV_EXISTING_MARKER))[0]; + if (envPath.includes(CONDA_ENV_CREATED_MARKER)) { + return envPath.substring(CONDA_ENV_CREATED_MARKER.length); + } + return envPath.substring(CONDA_ENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + async function createCondaEnv( workspace: WorkspaceFolder, command: string, args: string[], - progress?: CreateEnvironmentProgress, + progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { + progress.report({ + message: CreateEnv.Conda.creating, + }); + const deferred = createDeferred(); let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; if (getOSType() === OSType.Windows) { @@ -78,35 +108,20 @@ async function createCondaEnv( }, }); + const progressAndTelemetry = new CondaProgressAndTelemetry(progress); let condaEnvPath: string | undefined; out.subscribe( (value) => { const output = value.out.splitLines().join('\r\n'); traceLog(output); - if (output.includes(CONDA_ENV_CREATED_MARKER)) { - progress?.report({ - message: CreateEnv.Conda.created, - }); - try { - const envPath = output - .split(/\r?\n/g) - .map((s) => s.trim()) - .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER))[0]; - condaEnvPath = envPath.substring(CONDA_ENV_CREATED_MARKER.length); - } catch (ex) { - traceError('Parsing out environment path failed.'); - condaEnvPath = undefined; - } - } else if (output.includes(CONDA_INSTALLING_YML)) { - progress?.report({ - message: CreateEnv.Conda.installingPackages, - }); + if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { + condaEnvPath = getCondaEnvFromOutput(output); } + progressAndTelemetry.process(output); }, async (error) => { traceError('Error while running conda env creation script: ', error); deferred.reject(error); - await showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); }, () => { dispose(); @@ -139,42 +154,65 @@ function getExecutableCommand(condaPath: string): string { return path.join(path.dirname(condaPath), 'python'); } -async function createEnvironment( - options?: CreateEnvironmentOptions, - progress?: CreateEnvironmentProgress, - token?: CancellationToken, -): Promise { - progress?.report({ - message: CreateEnv.Conda.searching, - }); +async function createEnvironment(options?: CreateEnvironmentOptions): Promise { const conda = await getConda(); if (!conda) { return undefined; } - progress?.report({ - message: CreateEnv.Conda.waitingForWorkspace, - }); - const workspace = (await pickWorkspaceFolder({ token })) as WorkspaceFolder | undefined; + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; if (!workspace) { traceError('Workspace was not selected or found for creating virtual env.'); return undefined; } - progress?.report({ - message: CreateEnv.Conda.waitingForPython, - }); - const version = await pickPythonVersion(token); + const version = await pickPythonVersion(); if (!version) { traceError('Conda environments for use with python extension require Python.'); return undefined; } - progress?.report({ - message: CreateEnv.Conda.creating, - }); - const args = generateCommandArgs(version, options); - return createCondaEnv(workspace, getExecutableCommand(conda), args, progress, token); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => { + let hasError = false; + + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'conda', + pythonVersion: version, + }); + envPath = await createCondaEnv( + workspace, + getExecutableCommand(conda), + generateCommandArgs(version, options), + progress, + token, + ); + } catch (ex) { + traceError(ex); + hasError = true; + throw ex; + } finally { + if (hasError) { + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + } + } + return { path: envPath, uri: workspace.uri }; + }, + ); } export function condaCreationProvider(): CreateEnvironmentProvider { diff --git a/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts new file mode 100644 index 000000000000..49707c8ae31e --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; +export const CONDA_ENV_EXISTING_MARKER = 'EXISTING_CONDA_ENV:'; +export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; +export const CREATE_CONDA_FAILED_MARKER = 'CREATE_CONDA.ENV_FAILED_CREATION'; +export const CREATE_CONDA_INSTALLED_YML = 'CREATE_CONDA.INSTALLED_YML'; +export const CREATE_FAILED_INSTALL_YML = 'CREATE_CONDA.FAILED_INSTALL_YML'; + +export class CondaProgressAndTelemetry { + private condaCreatedReported = false; + + private condaFailedReported = false; + + private condaInstallingPackagesReported = false; + + private condaInstallingPackagesFailedReported = false; + + private condaInstalledPackagesReported = false; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public process(output: string): void { + if (!this.condaCreatedReported && output.includes(CONDA_ENV_CREATED_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } else if (!this.condaCreatedReported && output.includes(CONDA_ENV_EXISTING_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'existing', + }); + } else if (!this.condaFailedReported && output.includes(CREATE_CONDA_FAILED_MARKER)) { + this.condaFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'conda', + reason: 'other', + }); + } else if (!this.condaInstallingPackagesReported && output.includes(CONDA_INSTALLING_YML)) { + this.condaInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Conda.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } else if (!this.condaInstallingPackagesFailedReported && output.includes(CREATE_FAILED_INSTALL_YML)) { + this.condaInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } else if (!this.condaInstalledPackagesReported && output.includes(CREATE_CONDA_INSTALLED_YML)) { + this.condaInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } + } +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 9496d01a07fe..256bdb6b01fb 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -22,14 +22,14 @@ export async function getConda(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.7', '3.8', '3.9', '3.10'].map((v) => ({ + const items: QuickPickItem[] = ['3.10', '3.9', '3.8', '3.7'].map((v) => ({ label: `Python`, description: v, })); const version = await showQuickPick( items, { - title: CreateEnv.Conda.selectPythonQuickPickTitle, + placeHolder: CreateEnv.Conda.selectPythonQuickPickPlaceholder, }, token, ); diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index fbdc73f39258..8d9a677dc3f2 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -1,27 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, WorkspaceFolder } from 'vscode'; -import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import * as os from 'os'; +import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { CreateEnv } from '../../../common/utils/localize'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { traceError, traceLog } from '../../../logging'; -import { PythonEnvKind } from '../../base/info'; -import { IDiscoveryAPI } from '../../base/locator'; -import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from '../types'; +import { + CreateEnvironmentOptions, + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; import { EnvironmentType, PythonEnvironment } from '../../info'; - -export const VENV_CREATED_MARKER = 'CREATED_VENV:'; -export const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; -export const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; -export const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; -export const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; -export const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; -export const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +import { withProgress } from '../../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; +import { showErrorMessageWithLogs } from '../common/commonUtils'; function generateCommandArgs(options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -44,16 +45,37 @@ function generateCommandArgs(options?: CreateEnvironmentOptions): string[] { return command; } +function getVenvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(VENV_CREATED_MARKER) || s.startsWith(VENV_EXISTING_MARKER))[0]; + if (envPath.includes(VENV_CREATED_MARKER)) { + return envPath.substring(VENV_CREATED_MARKER.length); + } + return envPath.substring(VENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + async function createVenv( workspace: WorkspaceFolder, command: string, args: string[], - progress?: CreateEnvironmentProgress, + progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { - progress?.report({ + progress.report({ message: CreateEnv.Venv.creating, }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'venv', + pythonVersion: undefined, + }); + const deferred = createDeferred(); traceLog('Running Env creation script: ', [command, ...args]); const { out, dispose } = execObservable(command, args, { @@ -62,30 +84,16 @@ async function createVenv( cwd: workspace.uri.fsPath, }); + const progressAndTelemetry = new VenvProgressAndTelemetry(progress); let venvPath: string | undefined; out.subscribe( (value) => { - const output = value.out.split(/\r?\n/g).join('\r\n'); + const output = value.out.split(/\r?\n/g).join(os.EOL); traceLog(output); - if (output.includes(VENV_CREATED_MARKER)) { - progress?.report({ - message: CreateEnv.Venv.created, - }); - try { - const envPath = output - .split(/\r?\n/g) - .map((s) => s.trim()) - .filter((s) => s.startsWith(VENV_CREATED_MARKER))[0]; - venvPath = envPath.substring(VENV_CREATED_MARKER.length); - } catch (ex) { - traceError('Parsing out environment path failed.'); - venvPath = undefined; - } - } else if (output.includes(INSTALLING_REQUIREMENTS) || output.includes(INSTALLING_PYPROJECT)) { - progress?.report({ - message: CreateEnv.Venv.installingPackages, - }); + if (output.includes(VENV_CREATED_MARKER) || output.includes(VENV_EXISTING_MARKER)) { + venvPath = getVenvFromOutput(output); } + progressAndTelemetry.process(output); }, (error) => { traceError('Error while running venv creation script: ', error); @@ -102,38 +110,15 @@ async function createVenv( } export class VenvCreationProvider implements CreateEnvironmentProvider { - constructor( - private readonly discoveryApi: IDiscoveryAPI, - private readonly interpreterQuickPick: IInterpreterQuickPick, - ) {} - - public async createEnvironment( - options?: CreateEnvironmentOptions, - progress?: CreateEnvironmentProgress, - token?: CancellationToken, - ): Promise { - progress?.report({ - message: CreateEnv.Venv.waitingForWorkspace, - }); - - const workspace = (await pickWorkspaceFolder({ token })) as WorkspaceFolder | undefined; + constructor(private readonly interpreterQuickPick: IInterpreterQuickPick) {} + + public async createEnvironment(options?: CreateEnvironmentOptions): Promise { + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; if (workspace === undefined) { traceError('Workspace was not selected or found for creating virtual environment.'); return undefined; } - progress?.report({ - message: CreateEnv.Venv.waitingForPython, - }); - const interpreters = this.discoveryApi.getEnvs({ - kinds: [PythonEnvKind.MicrosoftStore, PythonEnvKind.OtherGlobal], - }); - - const args = generateCommandArgs(options); - if (interpreters.length === 1) { - return createVenv(workspace, interpreters[0].executable.filename, args, progress, token); - } - const interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( workspace.uri, (i: PythonEnvironment) => @@ -141,7 +126,46 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { ); if (interpreter) { - return createVenv(workspace, interpreter, args, progress, token); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => { + let hasError = false; + + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter) { + envPath = await createVenv( + workspace, + interpreter, + generateCommandArgs(options), + progress, + token, + ); + } + } catch (ex) { + traceError(ex); + hasError = true; + throw ex; + } finally { + if (hasError) { + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + } + } + + return { path: envPath, uri: workspace.uri }; + }, + ); } traceError('Virtual env creation requires an interpreter.'); diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts new file mode 100644 index 000000000000..423fb16b3110 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const VENV_CREATED_MARKER = 'CREATED_VENV:'; +export const VENV_EXISTING_MARKER = 'EXISTING_VENV:'; +export const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +export const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +export const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +export const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +export const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +export const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +export const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; +export const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; +export const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; +export const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; +export const PIP_UPGRADE_FAILED_MARKER = 'CREATE_VENV.PIP_UPGRADE_FAILED'; + +export class VenvProgressAndTelemetry { + private venvCreatedReported = false; + + private venvOrPipMissingReported = false; + + private venvFailedReported = false; + + private venvInstallingPackagesReported = false; + + private venvInstallingPackagesFailedReported = false; + + private venvInstalledPackagesReported = false; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public process(output: string): void { + if (!this.venvCreatedReported && output.includes(VENV_CREATED_MARKER)) { + this.venvCreatedReported = true; + this.progress.report({ + message: CreateEnv.Venv.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + } else if (!this.venvCreatedReported && output.includes(VENV_EXISTING_MARKER)) { + this.venvCreatedReported = true; + this.progress.report({ + message: CreateEnv.Venv.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + } else if (!this.venvOrPipMissingReported && output.includes(VENV_NOT_INSTALLED_MARKER)) { + this.venvOrPipMissingReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noVenv', + }); + } else if (!this.venvOrPipMissingReported && output.includes(PIP_NOT_INSTALLED_MARKER)) { + this.venvOrPipMissingReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noPip', + }); + } else if (!this.venvFailedReported && output.includes(CREATE_VENV_FAILED_MARKER)) { + this.venvFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'other', + }); + } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_REQUIREMENTS)) { + this.venvInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Venv.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_PYPROJECT)) { + this.venvInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Venv.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + } else if (!this.venvInstallingPackagesFailedReported && output.includes(PIP_UPGRADE_FAILED_MARKER)) { + this.venvInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_REQUIREMENTS_FAILED_MARKER)) { + this.venvInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_PYPROJECT_FAILED_MARKER)) { + this.venvInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_REQUIREMENTS_MARKER)) { + this.venvInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_PYPROJECT_MARKER)) { + this.venvInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + } + } +} diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts index 9e9a31799d09..6c844b8cfd02 100644 --- a/src/client/pythonEnvironments/creation/types.ts +++ b/src/client/pythonEnvironments/creation/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import { CancellationToken, Progress } from 'vscode'; +import { Progress, Uri } from 'vscode'; export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} @@ -10,12 +10,13 @@ export interface CreateEnvironmentOptions { ignoreSourceControl?: boolean; } +export interface CreateEnvironmentResult { + path: string | undefined; + uri: Uri | undefined; +} + export interface CreateEnvironmentProvider { - createEnvironment( - options?: CreateEnvironmentOptions, - progress?: CreateEnvironmentProgress, - token?: CancellationToken, - ): Promise; + createEnvironment(options?: CreateEnvironmentOptions): Promise; name: string; description: string; id: string; diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 4a611fcf3e7f..7f6f8b9c58a3 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -100,6 +100,13 @@ export enum EventName { TENSORBOARD_TORCH_PROFILER_IMPORT = 'TENSORBOARD.TORCH_PROFILER_IMPORT', TENSORBOARD_JUMP_TO_SOURCE_REQUEST = 'TENSORBOARD_JUMP_TO_SOURCE_REQUEST', TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND = 'TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND', + + ENVIRONMENT_CREATING = 'ENVIRONMENT.CREATING', + ENVIRONMENT_CREATED = 'ENVIRONMENT.CREATED', + ENVIRONMENT_FAILED = 'ENVIRONMENT.FAILED', + ENVIRONMENT_INSTALLING_PACKAGES = 'ENVIRONMENT.INSTALLING_PACKAGES', + ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', + ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', } export enum PlatformErrors { diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 1d1b2e076c13..4e4e9ba39649 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1974,18 +1974,97 @@ export interface IEventNamePropertyMapping { "tensorboard_jump_to_source_file_not_found" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND]: never | undefined; + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; + /** + * Telemetry event sent before creating an environment. + */ /* __GDPR__ - "query-expfeature" : { - "owner": "luabud", - "comment": "Logs queries to the experiment service by feature for metric calculations", - "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } - } - */ - /* __GDPR__ - "call-tas-error" : { - "owner": "luabud", - "comment": "Logs when calls to the experiment service fails", - "errortype": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Type of error when calling TAS (ServerError, NoResponse, etc.)"} - } - */ + "environment.creating" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "pythonVersion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CREATING]: { + environmentType: 'venv' | 'conda'; + pythonVersion: string | undefined; + }; + /** + * Telemetry event sent after creating an environment, but before attempting package installation. + */ + /* __GDPR__ + "environment.created" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CREATED]: { + environmentType: 'venv' | 'conda'; + reason: 'created' | 'existing'; + }; + /** + * Telemetry event sent if creating an environment failed. + */ + /* __GDPR__ + "environment.failed" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_FAILED]: { + environmentType: 'venv' | 'conda'; + reason: 'noVenv' | 'noPip' | 'other'; + }; + /** + * Telemetry event sent before installing packages. + */ + /* __GDPR__ + "environment.installing_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { + environmentType: 'venv' | 'conda'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + }; + /** + * Telemetry event sent after installing packages. + */ + /* __GDPR__ + "environment.installed_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLED_PACKAGES]: { + environmentType: 'venv' | 'conda'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + }; + /** + * Telemetry event sent if installing packages failed. + */ + /* __GDPR__ + "environment.installing_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED]: { + environmentType: 'venv' | 'conda'; + using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + }; + /* __GDPR__ + "query-expfeature" : { + "owner": "luabud", + "comment": "Logs queries to the experiment service by feature for metric calculations", + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } + } + */ + /* __GDPR__ + "call-tas-error" : { + "owner": "luabud", + "comment": "Logs when calls to the experiment service fails", + "errortype": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Type of error when calling TAS (ServerError, NoResponse, etc.)"} + } + */ } diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index 129776cc6a15..9671e393dc43 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -3,8 +3,9 @@ 'use strict'; -import { anything, deepEqual, instance, mock, reset, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; +import { anything, deepEqual, instance, mock, reset, verify, when } from 'ts-mockito'; import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { ApplicationShell } from '../../../client/common/application/applicationShell'; import { IApplicationShell } from '../../../client/common/application/types'; @@ -17,6 +18,7 @@ import { IComponentAdapter, IInterpreterHelper, IInterpreterService } from '../. import { InterpreterHelper } from '../../../client/interpreter/helpers'; import { VirtualEnvironmentPrompt } from '../../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as createEnvApi from '../../../client/pythonEnvironments/creation/createEnvApi'; suite('Virtual Environment Prompt', () => { class VirtualEnvironmentPromptTest extends VirtualEnvironmentPrompt { @@ -36,12 +38,15 @@ suite('Virtual Environment Prompt', () => { let componentAdapter: IComponentAdapter; let interpreterService: IInterpreterService; let environmentPrompt: VirtualEnvironmentPromptTest; + let isCreatingEnvironmentStub: sinon.SinonStub; setup(() => { persistentStateFactory = mock(PersistentStateFactory); helper = mock(InterpreterHelper); pythonPathUpdaterService = mock(PythonPathUpdaterService); componentAdapter = mock(); interpreterService = mock(); + isCreatingEnvironmentStub = sinon.stub(createEnvApi, 'isCreatingEnvironment'); + isCreatingEnvironmentStub.returns(false); when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ id: 'selected', path: 'path/to/selected', @@ -59,6 +64,10 @@ suite('Virtual Environment Prompt', () => { ); }); + teardown(() => { + sinon.restore(); + }); + test('User is notified if interpreter exists and only python path to global interpreter is specified in settings', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; @@ -256,4 +265,17 @@ suite('Virtual Environment Prompt', () => { verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); verify(appShell.showInformationMessage(anything(), ...prompts)).never(); }); + + test('If environment is being created, no notification is shown', async () => { + isCreatingEnvironmentStub.reset(); + isCreatingEnvironmentStub.returns(true); + + const resource = Uri.file('a'); + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + + await environmentPrompt.handleNewEnvironment(resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).never(); + verify(appShell.showInformationMessage(anything(), ...prompts)).never(); + }); }); diff --git a/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts deleted file mode 100644 index 165dff8c6b2b..000000000000 --- a/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as typemoq from 'typemoq'; -import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import * as createEnv from '../../../client/pythonEnvironments/creation/createEnvironment'; -import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvQuickPick'; -import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; - -suite('Create Environment Command Handler Tests', () => { - let showQuickPickStub: sinon.SinonStub; - let createEnvironmentStub: sinon.SinonStub; - - setup(() => { - showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); - createEnvironmentStub = sinon.stub(createEnv, 'createEnvironment'); - }); - - teardown(() => { - sinon.restore(); - }); - - test('No providers registered', async () => { - await handleCreateEnvironmentCommand([]); - - assert.isTrue(showQuickPickStub.notCalled); - assert.isTrue(createEnvironmentStub.notCalled); - }); - - test('Single environment creation provider registered', async () => { - const provider = typemoq.Mock.ofType(); - provider.setup((p) => p.name).returns(() => 'test'); - provider.setup((p) => p.id).returns(() => 'test-id'); - provider.setup((p) => p.description).returns(() => 'test-description'); - - await handleCreateEnvironmentCommand([provider.object]); - - assert.isTrue(showQuickPickStub.notCalled); - createEnvironmentStub.calledOnceWithExactly(provider.object, undefined); - }); - - test('Multiple environment creation providers registered', async () => { - const provider1 = typemoq.Mock.ofType(); - provider1.setup((p) => p.name).returns(() => 'test1'); - provider1.setup((p) => p.id).returns(() => 'test-id1'); - provider1.setup((p) => p.description).returns(() => 'test-description1'); - - const provider2 = typemoq.Mock.ofType(); - provider2.setup((p) => p.name).returns(() => 'test2'); - provider2.setup((p) => p.id).returns(() => 'test-id2'); - provider2.setup((p) => p.description).returns(() => 'test-description2'); - - showQuickPickStub.resolves({ - id: 'test-id2', - label: 'test2', - description: 'test-description2', - }); - - provider1.setup((p) => (p as any).then).returns(() => undefined); - provider2.setup((p) => (p as any).then).returns(() => undefined); - await handleCreateEnvironmentCommand([provider1.object, provider2.object]); - - assert.isTrue(showQuickPickStub.calledOnce); - createEnvironmentStub.calledOnceWithExactly(provider2.object, undefined); - }); -}); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts index 0e94e81ab38a..9c8e1af42b9a 100644 --- a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -1,71 +1,126 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { ProgressLocation, ProgressOptions } from 'vscode'; -import { Common, CreateEnv } from '../../../client/common/utils/localize'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import { createEnvironment } from '../../../client/pythonEnvironments/creation/createEnvironment'; -import { - CreateEnvironmentProgress, - CreateEnvironmentProvider, -} from '../../../client/pythonEnvironments/creation/types'; -import { Commands } from '../../../client/common/constants'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { onCreateEnvironmentStarted } from '../../../client/pythonEnvironments/creation/createEnvApi'; chaiUse(chaiAsPromised); suite('Create Environments Tests', () => { - let withProgressStub: sinon.SinonStub; - let progressMock: typemoq.IMock; + let showQuickPickStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let startedEventTriggered = false; + let exitedEventTriggered = false; setup(() => { - progressMock = typemoq.Mock.ofType(); - withProgressStub = sinon.stub(windowApis, 'withProgress'); - withProgressStub.callsFake(async (options: ProgressOptions, task) => { - assert.deepEqual(options, { - location: ProgressLocation.Notification, - title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, - cancellable: true, - }); - - await task(progressMock.object, undefined); - }); + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + startedEventTriggered = false; + exitedEventTriggered = false; + disposables.push( + onCreateEnvironmentStarted(() => { + startedEventTriggered = true; + }), + ); + disposables.push( + onCreateEnvironmentStarted(() => { + exitedEventTriggered = true; + }), + ); }); teardown(() => { - progressMock.reset(); sinon.restore(); + disposables.forEach((d) => d.dispose()); }); test('Successful environment creation', async () => { const provider = typemoq.Mock.ofType(); - provider - .setup((p) => p.createEnvironment(typemoq.It.isAny(), progressMock.object, undefined)) - .returns(() => Promise.resolve(undefined)); - progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusDone })).verifiable(typemoq.Times.once()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusError })).verifiable(typemoq.Times.never()); - await createEnvironment(provider.object); - - progressMock.verifyAll(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); provider.verifyAll(); }); test('Environment creation error', async () => { const provider = typemoq.Mock.ofType(); - provider - .setup((p) => p.createEnvironment(typemoq.It.isAny(), progressMock.object, undefined)) - .returns(() => Promise.reject()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusDone })).verifiable(typemoq.Times.never()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusError })).verifiable(typemoq.Times.once()); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.reject()); + provider.setup((p) => (p as any).then).returns(() => undefined); - await assert.isRejected(createEnvironment(provider.object)); + await assert.isRejected(handleCreateEnvironmentCommand([provider.object])); - progressMock.verifyAll(); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); provider.verifyAll(); }); + + test('No providers registered', async () => { + await handleCreateEnvironmentCommand([]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isFalse(startedEventTriggered); + assert.isFalse(exitedEventTriggered); + }); + + test('Single environment creation provider registered', async () => { + const provider = typemoq.Mock.ofType(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Multiple environment creation providers registered', async () => { + const provider1 = typemoq.Mock.ofType(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); }); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index fffcd2511a15..65b5affcbf8f 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -5,29 +5,35 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import { assert, use as chaiUse } from 'chai'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { CreateEnvironmentProvider } from '../../../../client/pythonEnvironments/creation/types'; +import * as typemoq from 'typemoq'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; import { - condaCreationProvider, - CONDA_ENV_CREATED_MARKER, -} from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/types'; +import { condaCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as condaUtils from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; import { Output } from '../../../../client/common/process/types'; import { createDeferred } from '../../../../client/common/utils/async'; import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { CONDA_ENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/condaProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; chaiUse(chaiAsPromised); suite('Conda Creation provider tests', () => { let condaProvider: CreateEnvironmentProvider; + let progressMock: typemoq.IMock; let getCondaStub: sinon.SinonStub; let pickPythonVersionStub: sinon.SinonStub; let pickWorkspaceFolderStub: sinon.SinonStub; let execObservableStub: sinon.SinonStub; - + let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; setup(() => { @@ -35,9 +41,12 @@ suite('Conda Creation provider tests', () => { getCondaStub = sinon.stub(condaUtils, 'getConda'); pickPythonVersionStub = sinon.stub(condaUtils, 'pickPythonVersion'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -72,11 +81,12 @@ suite('Conda Creation provider tests', () => { test('Create conda environment', async () => { getCondaStub.resolves('/usr/bin/conda/conda_bin/conda'); - pickWorkspaceFolderStub.resolves({ + const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', index: 0, - }); + }; + pickWorkspaceFolderStub.resolves(workspace1); pickPythonVersionStub.resolves('3.10'); const deferred = createDeferred(); @@ -100,6 +110,18 @@ suite('Conda Creation provider tests', () => { }; }); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + const promise = condaProvider.createEnvironment(); await deferred.promise; assert.isDefined(_next); @@ -107,7 +129,8 @@ suite('Conda Creation provider tests', () => { _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - assert.strictEqual(await promise, 'new_environment'); + assert.deepStrictEqual(await promise, { path: 'new_environment', uri: workspace1.uri }); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); test('Create conda environment failed', async () => { @@ -140,11 +163,24 @@ suite('Conda Creation provider tests', () => { }; }); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + const promise = condaProvider.createEnvironment(); await deferred.promise; assert.isDefined(_error); _error!('bad arguments'); _complete!(); await assert.isRejected(promise); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index e30c86e78a4f..1fb959f228ea 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -5,80 +5,46 @@ import * as path from 'path'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { CreateEnvironmentProvider } from '../../../../client/pythonEnvironments/creation/types'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; import { - VenvCreationProvider, - VENV_CREATED_MARKER, -} from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; -import { IDiscoveryAPI } from '../../../../client/pythonEnvironments/base/locator'; + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/types'; +import { VenvCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; import { IInterpreterQuickPick } from '../../../../client/interpreter/configuration/types'; import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; -import { PythonEnvKind, PythonEnvSource } from '../../../../client/pythonEnvironments/base/info'; -import { Architecture } from '../../../../client/common/utils/platform'; import { createDeferred } from '../../../../client/common/utils/async'; import { Output } from '../../../../client/common/process/types'; +import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; chaiUse(chaiAsPromised); -const python37 = { - name: 'Python 3.7', - kind: PythonEnvKind.System, - location: '/usr/bin/python3.7', - source: [PythonEnvSource.PathEnvVar], - executable: { - filename: '/usr/bin/python3.7', - ctime: 0, - mtime: 0, - sysPrefix: '', - }, - version: { - major: 3, - minor: 7, - micro: 7, - }, - arch: Architecture.x64, - distro: { - org: 'python', - }, -}; -const python38 = { - name: 'Python 3.8', - kind: PythonEnvKind.System, - location: '/usr/bin/python3.8', - source: [PythonEnvSource.PathEnvVar], - executable: { - filename: '/usr/bin/python3.8', - ctime: 0, - mtime: 0, - sysPrefix: '', - }, - version: { - major: 3, - minor: 8, - micro: 8, - }, - arch: Architecture.x64, - distro: { - org: 'python', - }, -}; - suite('venv Creation provider tests', () => { let venvProvider: CreateEnvironmentProvider; let pickWorkspaceFolderStub: sinon.SinonStub; - let discoveryApi: typemoq.IMock; let interpreterQuickPick: typemoq.IMock; + let progressMock: typemoq.IMock; let execObservableStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); - discoveryApi = typemoq.Mock.ofType(); interpreterQuickPick = typemoq.Mock.ofType(); - venvProvider = new VenvCreationProvider(discoveryApi.object, interpreterQuickPick.object); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + progressMock = typemoq.Mock.ofType(); + venvProvider = new VenvCreationProvider(interpreterQuickPick.object); }); teardown(() => { @@ -99,39 +65,27 @@ suite('venv Creation provider tests', () => { index: 0, }); - // Return multiple envs here to force user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python37, python38]) - .verifiable(typemoq.Times.once()); - interpreterQuickPick .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(typemoq.Times.once()); assert.isUndefined(await venvProvider.createEnvironment()); - discoveryApi.verifyAll(); interpreterQuickPick.verifyAll(); }); - test('Create venv with single global python', async () => { - pickWorkspaceFolderStub.resolves({ + test('Create venv with python selected by user', async () => { + const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', index: 0, - }); - - // Return single env here to skip user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python38]) - .verifiable(typemoq.Times.once()); + }; + pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.never()); + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); const deferred = createDeferred(); let _next: undefined | ((value: Output) => void); @@ -154,56 +108,17 @@ suite('venv Creation provider tests', () => { }; }); - const promise = venvProvider.createEnvironment(); - await deferred.promise; - assert.isDefined(_next); - assert.isDefined(_complete); - - _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); - _complete!(); - assert.strictEqual(await promise, 'new_environment'); - discoveryApi.verifyAll(); - interpreterQuickPick.verifyAll(); - }); - - test('Create venv with multiple global python', async () => { - pickWorkspaceFolderStub.resolves({ - uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), - name: 'workspace1', - index: 0, - }); - - // Return single env here to skip user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python37, python38]) - .verifiable(typemoq.Times.once()); - - interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(python38.executable.filename)) - .verifiable(typemoq.Times.once()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); - const deferred = createDeferred(); - let _next: undefined | ((value: Output) => void); - let _complete: undefined | (() => void); - execObservableStub.callsFake(() => { - deferred.resolve(); - return { - proc: undefined, - out: { - subscribe: ( - next?: (value: Output) => void, - _error?: (error: unknown) => void, - complete?: () => void, - ) => { - _next = next; - _complete = complete; - }, - }, - dispose: () => undefined, - }; - }); + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); const promise = venvProvider.createEnvironment(); await deferred.promise; @@ -212,9 +127,12 @@ suite('venv Creation provider tests', () => { _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - assert.strictEqual(await promise, 'new_environment'); - discoveryApi.verifyAll(); + + const actual = await promise; + assert.deepStrictEqual(actual, { path: 'new_environment', uri: workspace1.uri }); interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); test('Create venv failed', async () => { @@ -224,16 +142,10 @@ suite('venv Creation provider tests', () => { index: 0, }); - // Return single env here to skip user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python38]) - .verifiable(typemoq.Times.once()); - interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.never()); + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); const deferred = createDeferred(); let _error: undefined | ((error: unknown) => void); @@ -256,11 +168,24 @@ suite('venv Creation provider tests', () => { }; }); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + const promise = venvProvider.createEnvironment(); await deferred.promise; assert.isDefined(_error); _error!('bad arguments'); _complete!(); await assert.isRejected(promise); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); }); From e28b10db8845d2fc8be62f191c0fc400673b7c0f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 3 Oct 2022 13:41:39 -0700 Subject: [PATCH 54/59] release candidate-2022.16 (#19936) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8bd10ba6f104..7af82598db43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2022.15.0-dev", + "version": "2022.16.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2022.15.0-dev", + "version": "2022.16.0-rc", "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", diff --git a/package.json b/package.json index 51754e2f9fb2..cf8b4eb0daec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2022.15.0-dev", + "version": "2022.16.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 78bb43d80f6f887e1cd5b3ffa62ff037d9407c42 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 3 Oct 2022 14:10:14 -0700 Subject: [PATCH 55/59] post release (#19937) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7af82598db43..2b831b0cbe4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2022.16.0-rc", + "version": "2022.17.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2022.16.0-rc", + "version": "2022.17.0-dev", "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", diff --git a/package.json b/package.json index cf8b4eb0daec..a9de3c576e58 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2022.16.0-rc", + "version": "2022.17.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 90fb214b9f290efb41c731c236b6e54511041171 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 4 Oct 2022 14:17:04 -0700 Subject: [PATCH 56/59] Rename proposed api `environment` namespace to `environments` (#19938) --- src/client/deprecatedProposedApiTypes.ts | 4 +-- src/client/proposedApi.ts | 10 ++++---- src/client/proposedApiTypes.ts | 13 +++++----- src/test/proposedApi.unit.test.ts | 32 ++++++++++++------------ 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 4193c8b18212..864185483504 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -43,7 +43,7 @@ export interface ActiveEnvironmentChangedParams { */ export interface DeprecatedProposedAPI { /** - * @deprecated Use {@link ProposedExtensionAPI.environment} instead. This will soon be removed. + * @deprecated Use {@link ProposedExtensionAPI.environments} instead. This will soon be removed. */ environment: { /** @@ -131,7 +131,7 @@ export interface DeprecatedProposedAPI { */ onDidEnvironmentsChanged: Event; /** - * @deprecated Use {@link ProposedExtensionAPI.environment} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. + * @deprecated Use {@link ProposedExtensionAPI.environments} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. */ onDidActiveEnvironmentChanged: Event; }; diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 543c1dfb426a..46518755116e 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -151,7 +151,7 @@ export function buildProposedApi( ); /** - * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI.environment} instead. + * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI.environments} instead. */ let deprecatedEnvironmentsApi; try { @@ -163,7 +163,7 @@ export function buildProposedApi( } const proposed: ProposedExtensionAPI = { - environment: { + environments: { getActiveEnvironmentPath(resource?: Resource) { sendApiTelemetry('getActiveEnvironmentPath'); resource = resource && 'uri' in resource ? resource.uri : resource; @@ -213,8 +213,8 @@ export function buildProposedApi( sendApiTelemetry('resolveEnvironment'); return resolveEnvironment(path, discoveryApi); }, - get all(): Environment[] { - sendApiTelemetry('all'); + get known(): Environment[] { + sendApiTelemetry('known'); return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, async refreshEnvironments(options?: RefreshOptions) { @@ -227,8 +227,8 @@ export function buildProposedApi( sendApiTelemetry('onDidChangeEnvironments'); return onEnvironmentsChanged.event; }, - ...deprecatedEnvironmentsApi, }, + ...deprecatedEnvironmentsApi, }; return proposed; } diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index ee6e8e3384d7..229f37e1d087 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -6,9 +6,10 @@ import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; // https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs export interface ProposedExtensionAPI { - readonly environment: { + readonly environments: { /** - * Returns the environment configured by user in settings. + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root * scenario. If `undefined`, then the API returns what ever is set for the workspace. */ @@ -29,10 +30,10 @@ export interface ProposedExtensionAPI { */ readonly onDidChangeActiveEnvironmentPath: Event; /** - * Carries environments found by the extension at the time of fetching the property. Note this may not + * Carries environments known to the extension at the time of fetching the property. Note this may not * contain all environments in the system as a refresh might be going on. */ - readonly all: readonly Environment[]; + readonly known: readonly Environment[]; /** * This event is triggered when the known environment list changes, like when a environment * is found, existing environment is removed, or some details changed on an environment. @@ -53,7 +54,7 @@ export interface ProposedExtensionAPI { /** * Returns details for the given environment, or `undefined` if the env is invalid. * @param environment : Full path to environment folder or python executable for the environment. Can also pass - * the environment id or the environment itself. + * the environment itself. */ resolveEnvironment( environment: Environment | EnvironmentPath | string, @@ -63,7 +64,7 @@ export interface ProposedExtensionAPI { export type RefreshOptions = { /** - * Force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so * it's best to only use it if user manually triggers a refresh. */ forceRefresh?: boolean; diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 2bee65e70f65..8058c8ada4e3 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -76,7 +76,7 @@ suite('Proposed Extension API', () => { test('Provide an event to track when active environment details change', async () => { const events: ActiveEnvironmentPathChangeEvent[] = []; - proposed.environment.onDidChangeActiveEnvironmentPath((e) => { + proposed.environments.onDidChangeActiveEnvironmentPath((e) => { events.push(e); }); reportActiveInterpreterChanged({ path: 'path/to/environment', resource: undefined }); @@ -91,7 +91,7 @@ suite('Proposed Extension API', () => { configService .setup((c) => c.getSettings(undefined)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); - const actual = proposed.environment.getActiveEnvironmentPath(); + const actual = proposed.environments.getActiveEnvironmentPath(); assert.deepEqual(actual, ({ id: normCasePath(pythonPath), path: pythonPath, @@ -104,7 +104,7 @@ suite('Proposed Extension API', () => { configService .setup((c) => c.getSettings(undefined)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); - const actual = proposed.environment.getActiveEnvironmentPath(); + const actual = proposed.environments.getActiveEnvironmentPath(); assert.deepEqual(actual, ({ id: 'DEFAULT_PYTHON', path: pythonPath, @@ -118,7 +118,7 @@ suite('Proposed Extension API', () => { configService .setup((c) => c.getSettings(resource)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); - const actual = proposed.environment.getActiveEnvironmentPath(resource); + const actual = proposed.environments.getActiveEnvironmentPath(resource); assert.deepEqual(actual, ({ id: normCasePath(pythonPath), path: pythonPath, @@ -130,7 +130,7 @@ suite('Proposed Extension API', () => { const pythonPath = 'this/is/a/test/path'; discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(undefined)); - const actual = await proposed.environment.resolveEnvironment(pythonPath); + const actual = await proposed.environments.resolveEnvironment(pythonPath); expect(actual).to.be.equal(undefined); }); @@ -150,7 +150,7 @@ suite('Proposed Extension API', () => { }); discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - const actual = await proposed.environment.resolveEnvironment(pythonPath); + const actual = await proposed.environments.resolveEnvironment(pythonPath); assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); @@ -176,13 +176,13 @@ suite('Proposed Extension API', () => { }); discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - const actual = await proposed.environment.resolveEnvironment(convertCompleteEnvInfo(partialEnv)); + const actual = await proposed.environments.resolveEnvironment(convertCompleteEnvInfo(partialEnv)); assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); test('environments: no pythons found', () => { discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - const actual = proposed.environment.all; + const actual = proposed.environments.known; expect(actual).to.be.deep.equal([]); }); @@ -232,7 +232,7 @@ suite('Proposed Extension API', () => { }, ]; discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); - const actual = proposed.environment.all; + const actual = proposed.environments.known; const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual( actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)), @@ -244,7 +244,7 @@ suite('Proposed Extension API', () => { let events: EnvironmentsChangeEvent[] = []; let eventValues: EnvironmentsChangeEvent[] = []; let expectedEvents: EnvironmentsChangeEvent[] = []; - proposed.environment.onDidChangeEnvironments((e) => { + proposed.environments.onDidChangeEnvironments((e) => { events.push(e); }); const envs = [ @@ -336,7 +336,7 @@ suite('Proposed Extension API', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironmentPath('this/is/a/test/python/path'); + await proposed.environments.updateActiveEnvironmentPath('this/is/a/test/python/path'); interpreterPathService.verifyAll(); }); @@ -347,7 +347,7 @@ suite('Proposed Extension API', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironmentPath({ + await proposed.environments.updateActiveEnvironmentPath({ id: normCasePath('this/is/a/test/python/path'), path: 'this/is/a/test/python/path', }); @@ -362,7 +362,7 @@ suite('Proposed Extension API', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironmentPath('this/is/a/test/python/path', uri); + await proposed.environments.updateActiveEnvironmentPath('this/is/a/test/python/path', uri); interpreterPathService.verifyAll(); }); @@ -379,7 +379,7 @@ suite('Proposed Extension API', () => { index: 0, }; - await proposed.environment.updateActiveEnvironmentPath('this/is/a/test/python/path', workspace); + await proposed.environments.updateActiveEnvironmentPath('this/is/a/test/python/path', workspace); interpreterPathService.verifyAll(); }); @@ -390,7 +390,7 @@ suite('Proposed Extension API', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.refreshEnvironments(); + await proposed.environments.refreshEnvironments(); discoverAPI.verifyAll(); }); @@ -401,7 +401,7 @@ suite('Proposed Extension API', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.refreshEnvironments({ forceRefresh: true }); + await proposed.environments.refreshEnvironments({ forceRefresh: true }); discoverAPI.verifyAll(); }); From 6c317df9a42917c9338dc2b88ee5db42844e8444 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 4 Oct 2022 15:13:04 -0700 Subject: [PATCH 57/59] Fix deprecated API bug (#19944) --- src/client/deprecatedProposedApi.ts | 28 +++++++++++++++--------- src/client/deprecatedProposedApiTypes.ts | 9 ++++++++ src/client/proposedApi.ts | 17 ++++++-------- src/test/proposedApi.unit.test.ts | 16 +++++--------- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index 459f2b1bf529..e63670e4bf1b 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -86,15 +86,23 @@ export function buildDeprecatedProposedApi( const proposed: DeprecatedProposedAPI = { environment: { async getExecutionDetails(resource?: Resource) { - sendApiTelemetry('getExecutionDetails'); + sendApiTelemetry('deprecated.getExecutionDetails'); const env = await interpreterService.getActiveInterpreter(resource); return env ? { execCommand: [env.path] } : { execCommand: undefined }; }, + async getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('deprecated.getActiveEnvironmentPath'); + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return getEnvPath(env.path, env.envPath); + }, async getEnvironmentDetails( path: string, options?: EnvironmentDetailsOptions, ): Promise { - sendApiTelemetry('getEnvironmentDetails'); + sendApiTelemetry('deprecated.getEnvironmentDetails'); let env: PythonEnvInfo | undefined; if (options?.useCache) { env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); @@ -118,38 +126,38 @@ export function buildDeprecatedProposedApi( }; }, getEnvironmentPaths() { - sendApiTelemetry('getEnvironmentPaths'); + sendApiTelemetry('deprecated.getEnvironmentPaths'); const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); return Promise.resolve(paths); }, setActiveEnvironment(path: string, resource?: Resource): Promise { - sendApiTelemetry('setActiveEnvironment'); + sendApiTelemetry('deprecated.setActiveEnvironment'); return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, async refreshEnvironment() { - sendApiTelemetry('refreshEnvironment'); + sendApiTelemetry('deprecated.refreshEnvironment'); await discoveryApi.triggerRefresh(); const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); return Promise.resolve(paths); }, getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { - sendApiTelemetry('getRefreshPromise'); + sendApiTelemetry('deprecated.getRefreshPromise'); return discoveryApi.getRefreshPromise(options); }, get onDidChangeExecutionDetails() { - sendApiTelemetry('onDidChangeExecutionDetails', false); + sendApiTelemetry('deprecated.onDidChangeExecutionDetails', false); return interpreterService.onDidChangeInterpreterConfiguration; }, get onDidEnvironmentsChanged() { - sendApiTelemetry('onDidEnvironmentsChanged', false); + sendApiTelemetry('deprecated.onDidEnvironmentsChanged', false); return onDidInterpretersChangedEvent.event; }, get onDidActiveEnvironmentChanged() { - sendApiTelemetry('onDidActiveEnvironmentChanged', false); + sendApiTelemetry('deprecated.onDidActiveEnvironmentChanged', false); return onDidActiveInterpreterChangedEvent.event; }, get onRefreshProgress() { - sendApiTelemetry('onRefreshProgress', false); + sendApiTelemetry('deprecated.onRefreshProgress', false); return discoveryApi.onProgress; }, }, diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 864185483504..f2a2cbe040af 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -74,6 +74,15 @@ export interface DeprecatedProposedAPI { */ execCommand: string[] | undefined; }>; + /** + * Returns the path to the python binary selected by the user or as in the settings. + * This is just the path to the python binary, this does not provide activation or any + * other activation command. The `resource` if provided will be used to determine the + * python binary in a multi-root scenario. If resource is `undefined` then the API + * returns what ever is set for the workspace. + * @param resource : Uri of a file or workspace + */ + getActiveEnvironmentPath(resource?: Resource): Promise; /** * Returns details for the given interpreter. Details such as absolute interpreter path, * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 46518755116e..288d75a0f2b5 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -32,6 +32,7 @@ import { reportActiveInterpreterChangedDeprecated, reportInterpretersChanged, } from './deprecatedProposedApi'; +import { DeprecatedProposedAPI } from './deprecatedProposedApiTypes'; type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; @@ -151,18 +152,19 @@ export function buildProposedApi( ); /** - * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI.environments} instead. + * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI} instead. */ - let deprecatedEnvironmentsApi; + let deprecatedProposedApi; try { - deprecatedEnvironmentsApi = { ...buildDeprecatedProposedApi(discoveryApi, serviceContainer).environment }; + deprecatedProposedApi = { ...buildDeprecatedProposedApi(discoveryApi, serviceContainer) }; } catch (ex) { - deprecatedEnvironmentsApi = {}; + deprecatedProposedApi = {} as DeprecatedProposedAPI; // Errors out only in case of testing. // Also, these APIs no longer supported, no need to log error. } - const proposed: ProposedExtensionAPI = { + const proposed: ProposedExtensionAPI & DeprecatedProposedAPI = { + ...deprecatedProposedApi, environments: { getActiveEnvironmentPath(resource?: Resource) { sendApiTelemetry('getActiveEnvironmentPath'); @@ -172,10 +174,6 @@ export function buildProposedApi( return { id, path, - /** - * @deprecated Only provided for backwards compatibility and will soon be removed. - */ - pathType: 'interpreterPath', }; }, updateActiveEnvironmentPath( @@ -228,7 +226,6 @@ export function buildProposedApi( return onEnvironmentsChanged.event; }, }, - ...deprecatedEnvironmentsApi, }; return proposed; } diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 8058c8ada4e3..a4358c0830fc 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -29,7 +29,6 @@ import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/ba import { normCasePath } from '../client/common/platform/fs-paths'; import { ActiveEnvironmentPathChangeEvent, - EnvironmentPath, EnvironmentsChangeEvent, ProposedExtensionAPI, } from '../client/proposedApiTypes'; @@ -92,11 +91,10 @@ suite('Proposed Extension API', () => { .setup((c) => c.getSettings(undefined)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); const actual = proposed.environments.getActiveEnvironmentPath(); - assert.deepEqual(actual, ({ + assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath, - pathType: 'interpreterPath', - } as unknown) as EnvironmentPath); + }); }); test('getActiveEnvironmentPath: default python', () => { @@ -105,11 +103,10 @@ suite('Proposed Extension API', () => { .setup((c) => c.getSettings(undefined)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); const actual = proposed.environments.getActiveEnvironmentPath(); - assert.deepEqual(actual, ({ + assert.deepEqual(actual, { id: 'DEFAULT_PYTHON', path: pythonPath, - pathType: 'interpreterPath', - } as unknown) as EnvironmentPath); + }); }); test('getActiveEnvironmentPath: With resource', () => { @@ -119,11 +116,10 @@ suite('Proposed Extension API', () => { .setup((c) => c.getSettings(resource)) .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); const actual = proposed.environments.getActiveEnvironmentPath(resource); - assert.deepEqual(actual, ({ + assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath, - pathType: 'interpreterPath', - } as unknown) as EnvironmentPath); + }); }); test('resolveEnvironment: invalid environment (when passed as string)', async () => { From ebfd704da25b2b7783553d6414737075e42b543f Mon Sep 17 00:00:00 2001 From: Heejae Chang <1333179+heejaechang@users.noreply.github.com> Date: Wed, 5 Oct 2022 21:00:42 -0700 Subject: [PATCH 58/59] Removed old notebook experiment option that is no longer used (#19951) this PR is a part of bigger effort of removing old notebook experiments. https://github.com/microsoft/vscode-jupyter-lsp-middleware/pull/60 https://github.com/microsoft/vscode-jupyter/pull/11545 --- package-lock.json | 54 +++++++++---------- package.json | 8 +-- package.nls.json | 1 - .../activation/node/lspNotebooksExperiment.ts | 2 +- src/client/common/configSettings.ts | 3 -- src/client/common/types.ts | 1 - 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b831b0cbe4f..0ea2f8c850e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", - "@vscode/jupyter-lsp-middleware": "^0.2.46", + "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", "fs-extra": "^10.0.1", @@ -1107,15 +1107,15 @@ } }, "node_modules/@vscode/jupyter-lsp-middleware": { - "version": "0.2.46", - "resolved": "https://registry.npmjs.org/@vscode/jupyter-lsp-middleware/-/jupyter-lsp-middleware-0.2.46.tgz", - "integrity": "sha512-xP7E8YwdwS9+pCdLaoZcvtWy6vJpuCIN990IgVjKRrGAqwpJzSojuZs1Lai7YYkOqWYfa1xjsNfZ+kw3N2gzlw==", + "version": "0.2.50", + "resolved": "https://registry.npmjs.org/@vscode/jupyter-lsp-middleware/-/jupyter-lsp-middleware-0.2.50.tgz", + "integrity": "sha512-oOEpRZOJdKjByRMkUDVdGlQDiEO4/Mjr88u5zqktaS/4h0NtX8Hk6+HNQwENp4ur3Dpu47gD8wOTCrkOWzbHlA==", "dependencies": { - "@vscode/lsp-notebook-concat": "^0.1.13", + "@vscode/lsp-notebook-concat": "^0.1.16", "fast-myers-diff": "^3.0.1", "sha.js": "^2.4.11", - "vscode-languageclient": "^8.0.2-next.3", - "vscode-languageserver-protocol": "^3.17.2-next.3", + "vscode-languageclient": "^8.0.2-next.4", + "vscode-languageserver-protocol": "^3.17.2-next.5", "vscode-uri": "^3.0.2" }, "engines": { @@ -1123,12 +1123,12 @@ } }, "node_modules/@vscode/lsp-notebook-concat": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.13.tgz", - "integrity": "sha512-+w5AAYMc/lC3kKagLWuH0t+/0at24Fm3SsksKfWeKRH/Mg4SQC5TNv5xy3SS8nDeyxB0JIx0vp6WXmu+PUz4tA==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.16.tgz", + "integrity": "sha512-jN2ut22GR/xelxHx2W9U+uZoylHGCezsNmsMYn20LgVHTcJMGL+4bL5PJeh63yo6P5XjAPtoeeymvp5EafJV+w==", "dependencies": { "object-hash": "^3.0.0", - "vscode-languageserver-protocol": "^3.17.2-next.3", + "vscode-languageserver-protocol": "^3.17.2-next.5", "vscode-uri": "^3.0.2" } }, @@ -14601,9 +14601,9 @@ } }, "node_modules/vscode-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz", - "integrity": "sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz", + "integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==" }, "node_modules/watchpack": { "version": "2.3.1", @@ -16174,25 +16174,25 @@ } }, "@vscode/jupyter-lsp-middleware": { - "version": "0.2.46", - "resolved": "https://registry.npmjs.org/@vscode/jupyter-lsp-middleware/-/jupyter-lsp-middleware-0.2.46.tgz", - "integrity": "sha512-xP7E8YwdwS9+pCdLaoZcvtWy6vJpuCIN990IgVjKRrGAqwpJzSojuZs1Lai7YYkOqWYfa1xjsNfZ+kw3N2gzlw==", + "version": "0.2.50", + "resolved": "https://registry.npmjs.org/@vscode/jupyter-lsp-middleware/-/jupyter-lsp-middleware-0.2.50.tgz", + "integrity": "sha512-oOEpRZOJdKjByRMkUDVdGlQDiEO4/Mjr88u5zqktaS/4h0NtX8Hk6+HNQwENp4ur3Dpu47gD8wOTCrkOWzbHlA==", "requires": { - "@vscode/lsp-notebook-concat": "^0.1.13", + "@vscode/lsp-notebook-concat": "^0.1.16", "fast-myers-diff": "^3.0.1", "sha.js": "^2.4.11", - "vscode-languageclient": "^8.0.2-next.3", - "vscode-languageserver-protocol": "^3.17.2-next.3", + "vscode-languageclient": "^8.0.2-next.4", + "vscode-languageserver-protocol": "^3.17.2-next.5", "vscode-uri": "^3.0.2" } }, "@vscode/lsp-notebook-concat": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.13.tgz", - "integrity": "sha512-+w5AAYMc/lC3kKagLWuH0t+/0at24Fm3SsksKfWeKRH/Mg4SQC5TNv5xy3SS8nDeyxB0JIx0vp6WXmu+PUz4tA==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.16.tgz", + "integrity": "sha512-jN2ut22GR/xelxHx2W9U+uZoylHGCezsNmsMYn20LgVHTcJMGL+4bL5PJeh63yo6P5XjAPtoeeymvp5EafJV+w==", "requires": { "object-hash": "^3.0.0", - "vscode-languageserver-protocol": "^3.17.2-next.3", + "vscode-languageserver-protocol": "^3.17.2-next.5", "vscode-uri": "^3.0.2" } }, @@ -26853,9 +26853,9 @@ } }, "vscode-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz", - "integrity": "sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz", + "integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==" }, "watchpack": { "version": "2.3.1", diff --git a/package.json b/package.json index a9de3c576e58..e2a157fc37cf 100644 --- a/package.json +++ b/package.json @@ -920,12 +920,6 @@ "scope": "machine-overridable", "type": "string" }, - "python.pylanceLspNotebooksEnabled": { - "type": "boolean", - "default": true, - "description": "%python.pylanceLspNotebooksEnabled.description%", - "scope": "machine" - }, "python.sortImports.args": { "default": [], "description": "%python.sortImports.args.description%", @@ -1797,7 +1791,7 @@ "webpack": "webpack" }, "dependencies": { - "@vscode/jupyter-lsp-middleware": "^0.2.46", + "@vscode/jupyter-lsp-middleware": "^0.2.50", "@vscode/extension-telemetry": "^0.6.2", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", diff --git a/package.nls.json b/package.nls.json index 7a39d545fab6..7dbd2b942c6a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -94,7 +94,6 @@ "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", - "python.pylanceLspNotebooksEnabled.description": "Determines if Pylance's experimental LSP notebooks support is used or not.", "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.sortImports.path.description": "Path to isort script, default using inner version", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", diff --git a/src/client/activation/node/lspNotebooksExperiment.ts b/src/client/activation/node/lspNotebooksExperiment.ts index 7cadc1044a2b..d469cfb112df 100644 --- a/src/client/activation/node/lspNotebooksExperiment.ts +++ b/src/client/activation/node/lspNotebooksExperiment.ts @@ -68,7 +68,7 @@ export class LspNotebooksExperiment implements IExtensionSingleActivationService private updateExperimentSupport(): void { const wasInExperiment = this.isInExperiment; - const isInTreatmentGroup = this.configurationService.getSettings().pylanceLspNotebooksEnabled; + const isInTreatmentGroup = true; const languageServerType = this.configurationService.getSettings().languageServer; this.isInExperiment = false; diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 52ed1cbeda79..93a3f631c67a 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -122,8 +122,6 @@ export class PythonSettings implements IPythonSettings { public globalModuleInstallation = false; - public pylanceLspNotebooksEnabled = false; - public experiments!: IExperiments; public languageServer: LanguageServerType = LanguageServerType.Node; @@ -313,7 +311,6 @@ export class PythonSettings implements IPythonSettings { } this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - this.pylanceLspNotebooksEnabled = pythonSettings.get('pylanceLspNotebooksEnabled') === true; const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; if (this.sortImports) { diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 66c91b13444d..18b6e1481746 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -196,7 +196,6 @@ export interface IPythonSettings { readonly sortImports: ISortImportSettings; readonly envFile: string; readonly globalModuleInstallation: boolean; - readonly pylanceLspNotebooksEnabled: boolean; readonly experiments: IExperiments; readonly languageServer: LanguageServerType; readonly languageServerIsDefault: boolean; From 8941c6101eaaa21221096f9015e7609d5d4fd801 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 6 Oct 2022 16:14:05 -0700 Subject: [PATCH 59/59] Log more for issue label action --- .github/workflows/issue-labels.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index b10983dd0706..9f1344e346bb 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -47,7 +47,10 @@ jobs: issue_number: context.issue.number, }) .then((result) => result.data.assignees.map((a) => a.login)); + console.log('Known triagers:', JSON.stringify(knownTriagers)); + console.log('Current assignees:', JSON.stringify(currentAssignees)); const assigneesToRemove = currentAssignees.filter(a => !knownTriagers.includes(a)); + console.log('Assignees to remove:', JSON.stringify(assigneesToRemove)); github.rest.issues.removeAssignees({ owner: context.repo.owner, repo: context.repo.repo,