diff --git a/.eslintignore b/.eslintignore index 20f49860c5a0..7ba146c7d8e4 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 @@ -247,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/.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 diff --git a/.github/workflows/getLabels.js b/.github/workflows/getLabels.js new file mode 100644 index 000000000000..99060e7205eb --- /dev/null +++ b/.github/workflows/getLabels.js @@ -0,0 +1,25 @@ +/** + * 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'); +const github = new Octokit(); +github.rest.issues + .listLabelsForRepo({ + owner: 'microsoft', + repo: 'vscode-python', + per_page: 100, + }) + .then((result) => { + const labels = result.data.map((label) => label.name); + 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 d743d437428a..9f1344e346bb 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","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: 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 @@ -34,6 +39,34 @@ jobs: issue_number: context.issue.number, labels: ['triage-needed'] }) + const knownTriagers = ${{ env.TRIAGERS }} + 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)); + 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, + 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.') } + 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, + }) + } + } 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 diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml new file mode 100644 index 000000000000..95a014790d75 --- /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/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/package-lock.json b/package-lock.json index 1006c8056e19..0ea2f8c850e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "python", - "version": "2022.13.0-dev", + "version": "2022.17.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2022.13.0-dev", + "version": "2022.17.0-dev", "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", @@ -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", @@ -42,7 +43,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" @@ -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", @@ -73,7 +75,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 +121,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 +585,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", @@ -876,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", @@ -1142,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": { @@ -1158,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" } }, @@ -1179,39 +1144,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 +1194,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 +1451,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", @@ -2118,11 +2065,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": { @@ -3275,12 +3222,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", @@ -6038,9 +5979,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", @@ -9218,9 +9159,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" }, @@ -12621,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": "*" } @@ -13071,11 +13011,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": { @@ -13558,16 +13498,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,78 +14589,21 @@ "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", - "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" } }, - "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", - "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", @@ -15874,41 +15747,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", @@ -16158,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", @@ -16330,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" } }, @@ -16363,36 +16207,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 +16248,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 +16478,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", @@ -17119,11 +16948,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": { @@ -18035,12 +17864,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", @@ -20263,9 +20086,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", @@ -22751,9 +22574,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" }, @@ -25414,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", @@ -25779,11 +25601,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": { @@ -26142,16 +25964,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,60 +26844,18 @@ } } }, - "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", - "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" - } - }, - "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" + "tas-client": "0.1.58" } }, "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 48919bb4c1bb..e2a157fc37cf 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.17.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, @@ -83,6 +83,7 @@ "onCommand:python.enableSourceMapSupport", "onCommand:python.launchTensorBoard", "onCommand:python.clearCacheAndReload", + "onCommand:python.createEnvironment", "onWalkthrough:pythonWelcome", "onWalkthrough:pythonWelcomeWithDS", "onWalkthrough:pythonDataScienceWelcome", @@ -271,6 +272,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", @@ -914,15 +920,6 @@ "scope": "machine-overridable", "type": "string" }, - "python.pylanceLspNotebooksEnabled": { - "type": "boolean", - "default": false, - "description": "%python.pylanceLspNotebooksEnabled.description%", - "scope": "machine", - "tags": [ - "experimental" - ] - }, "python.sortImports.args": { "default": [], "description": "%python.sortImports.args.description%", @@ -1198,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": {}, @@ -1540,6 +1540,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", @@ -1785,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", @@ -1805,6 +1811,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", @@ -1818,7 +1825,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" @@ -1841,15 +1848,16 @@ "@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", - "@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 +1903,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", diff --git a/package.nls.json b/package.nls.json index 54cb7a7fd9a4..7dbd2b942c6a 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", @@ -19,12 +20,12 @@ "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", "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.", @@ -93,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/pythonFiles/create_conda.py b/pythonFiles/create_conda.py new file mode 100644 index 000000000000..9a34de47d51f --- /dev/null +++ b/pythonFiles/create_conda.py @@ -0,0 +1,131 @@ +# 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", + ) + print("CREATE_CONDA.INSTALLED_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 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, + "-m", + "conda", + "create", + "--yes", + "--prefix", + args.name, + f"python={args.python}", + ], + "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) + + 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..1f31abc5cc87 --- /dev/null +++ b/pythonFiles/create_venv.py @@ -0,0 +1,136 @@ +# 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) and file_exists(get_venv_path(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: + 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", + ) + 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: + 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 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"EXISTING_VENV:{venv_path}") + else: + 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__": + main(sys.argv[1:]) 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=()): 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/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/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/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..815ca73ff7eb 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 as string; + return pylanceVersion !== undefined && semver.prerelease(pylanceVersion)?.includes('dev') === true; + } + + 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/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/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/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/client/apiTypes.ts b/src/client/apiTypes.ts index 847b215bce49..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,141 +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 RefreshEnvironmentsOptions { - clearCache?: boolean; -} - -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(options?: RefreshEnvironmentsOptions): 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/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/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/constants.ts b/src/client/common/constants.ts index 30ebb88c36a9..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 @@ -85,7 +86,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/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/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/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/client/common/types.ts b/src/client/common/types.ts index 571a9a01b8a2..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; @@ -428,6 +427,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/common/utils/localize.ts b/src/client/common/utils/localize.ts index a45c93270c5f..e1e2f8d71184 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'); @@ -317,10 +318,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...', @@ -553,3 +550,70 @@ 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 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 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 pickWorkspacePlaceholder = localize( + 'createEnv.workspaceQuickPick.placeholder', + 'Select a workspace to create environment', + ); + + 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 errorCreatingEnvironment = localize( + 'createEnv.venv.errorCreatingEnvironment', + 'Error while creating virtual environment.', + ); + 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 const error = localize('createEnv.venv.error', 'Creating virtual environment failed with error.'); + } + + 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 selectPythonQuickPickPlaceholder = localize( + 'createEnv.conda.pythonSelection.placeholder', + 'Please select the version of Python to install in the environment', + ); + 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/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index 1cb6bad11d66..12aafe3c8099 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -46,8 +46,8 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T; - placeholder: string; + activeItem?: T | Promise; + placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; matchOnDetail?: 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/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..07a4b4c4acc6 --- /dev/null +++ b/src/client/common/vscodeApis/windowApis.ts @@ -0,0 +1,63 @@ +// 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 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, +): 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/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/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index 450b7e5ee032..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,15 +48,15 @@ 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', 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, }); @@ -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', 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 07d5522f27ca..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_ENV: 'development', - }, - args: ['run', '--no-debugger'], - 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/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts new file mode 100644 index 000000000000..e63670e4bf1b --- /dev/null +++ b/src/client/deprecatedProposedApi.ts @@ -0,0 +1,166 @@ +// 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 { traceVerbose } from './logging'; +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); + const warningLogged = new Set(); + function sendApiTelemetry(apiName: string, warnLog = true) { + extensions + .determineExtensionFromCallStack() + .then((info) => { + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + 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(); + } + + const proposed: DeprecatedProposedAPI = { + environment: { + async getExecutionDetails(resource?: Resource) { + 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('deprecated.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('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('deprecated.setActiveEnvironment'); + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + async 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('deprecated.getRefreshPromise'); + return discoveryApi.getRefreshPromise(options); + }, + get onDidChangeExecutionDetails() { + sendApiTelemetry('deprecated.onDidChangeExecutionDetails', false); + return interpreterService.onDidChangeInterpreterConfiguration; + }, + get onDidEnvironmentsChanged() { + sendApiTelemetry('deprecated.onDidEnvironmentsChanged', false); + return onDidInterpretersChangedEvent.event; + }, + get onDidActiveEnvironmentChanged() { + sendApiTelemetry('deprecated.onDidActiveEnvironmentChanged', false); + return onDidActiveInterpreterChangedEvent.event; + }, + get onRefreshProgress() { + sendApiTelemetry('deprecated.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..f2a2cbe040af --- /dev/null +++ b/src/client/deprecatedProposedApiTypes.ts @@ -0,0 +1,147 @@ +// 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.environments} 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; + }>; + /** + * 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; + /** + * @deprecated Use {@link ProposedExtensionAPI.environments} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event; + }; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 312e99a38683..04ad91f80c84 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -35,17 +35,18 @@ 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'; 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); @@ -128,6 +129,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..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'; @@ -61,6 +67,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 +103,16 @@ 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, + ); + const interpreterPathService: IInterpreterPathService = ext.legacyIOC.serviceContainer.get( + IInterpreterPathService, + ); + registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService); +} + /// ////////////////////////// // old activation code 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/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c918e99bffbe..1d7f3b910f52 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, @@ -80,11 +81,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, @@ -131,40 +127,43 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { public async _pickInterpreter( 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. 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, 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: 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, + title, customButtonSetups: [ - { - button: this.hardRefreshButton, - callback: (quickpickInput) => { - this.refreshButtonCallback(quickpickInput, true); - }, - }, { button: this.refreshButton, callback: (quickpickInput) => { - this.refreshButtonCallback(quickpickInput, false); + this.refreshButtonCallback(quickpickInput); }, }, ], @@ -190,10 +189,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); }, }, }); @@ -215,26 +214,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, @@ -288,10 +294,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) => { @@ -315,10 +322,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( @@ -418,7 +429,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 +437,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { }, ]; this.interpreterService - .triggerRefresh(undefined, { clearCache }) + .triggerRefresh() .finally(() => { - input.buttons = [this.hardRefreshButton, this.refreshButton]; + input.buttons = [this.refreshButton]; }) .ignoreErrors(); } @@ -487,7 +498,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 @@ -497,6 +508,17 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { } } + 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, params), 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, @@ -506,7 +528,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/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/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 2621a297defe..264ac523538d 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -65,3 +65,21 @@ 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, + params?: { + /** + * Specify `null` if a placeholder is not required. + */ + placeholder?: string | null; + /** + * Specify `null` if a title is not required. + */ + title?: string | null; + }, + ): Promise; +} 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/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 1f563125162d..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)); @@ -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/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 6d36a4b9277d..cdcd8718fd1d 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -15,20 +15,19 @@ 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'; import { IInterpreterComparer, + IInterpreterQuickPick, IInterpreterSelector, 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'; @@ -58,10 +57,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IExtensionSingleActivationService, ResetInterpreterCommand, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - SetShebangInterpreterCommand, - ); + serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); @@ -79,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/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/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 faa1bb75c4bc..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,11 +51,10 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities readonly applicationShell: IApplicationShell, lspNotebooksExperiment: LspNotebooksExperiment, ) { - super(); - this.analysisOptions = new NodeLanguageServerAnalysisOptions( outputChannel, workspaceService, + experimentService, lspNotebooksExperiment, ); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); 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/client/proposedApi.ts b/src/client/proposedApi.ts index e4ac6fd83caa..288d75a0f2b5 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -1,119 +1,341 @@ +/* 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, - RefreshEnvironmentsOptions, -} 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 { + ActiveEnvironmentPathChangeEvent, + Environment, + EnvironmentsChangeEvent, + ProposedExtensionAPI, + ResolvedEnvironment, + RefreshOptions, + Resource, + EnvironmentType, + EnvironmentTools, + EnvironmentPath, +} 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, traceVerbose } from './logging'; +import { normCasePath } from './common/platform/fs-paths'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { + buildDeprecatedProposedApi, + reportActiveInterpreterChangedDeprecated, + reportInterpretersChanged, +} from './deprecatedProposedApi'; +import { DeprecatedProposedAPI } from './deprecatedProposedApiTypes'; -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, + }); + traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + }) + .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, + ); - const proposed: IProposedExtensionAPI = { - environment: { - async getExecutionDetails(resource?: Resource) { - const env = await interpreterService.getActiveInterpreter(resource); - return env ? { execCommand: [env.path] } : { execCommand: undefined }; + /** + * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI} instead. + */ + let deprecatedProposedApi; + try { + deprecatedProposedApi = { ...buildDeprecatedProposedApi(discoveryApi, serviceContainer) }; + } catch (ex) { + deprecatedProposedApi = {} as DeprecatedProposedAPI; + // Errors out only in case of testing. + // Also, these APIs no longer supported, no need to log error. + } + + const proposed: ProposedExtensionAPI & DeprecatedProposedAPI = { + ...deprecatedProposedApi, + environments: { + 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, + }; }, - async getActiveEnvironmentPath(resource?: Resource) { - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return getEnvPath(env.path, env.envPath); + 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); }, - 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 onDidChangeActiveEnvironmentPath() { + sendApiTelemetry('onDidChangeActiveEnvironmentPath'); + return onDidActiveInterpreterChangedEvent.event; + }, + 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. + // 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, - }, - }; + sendApiTelemetry('resolveEnvironment'); + return resolveEnvironment(path, discoveryApi); }, - getEnvironmentPaths() { - const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); - return Promise.resolve(paths); - }, - setActiveEnvironment(path: string, resource?: Resource): Promise { - return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + get known(): Environment[] { + sendApiTelemetry('known'); + return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, - async refreshEnvironment(options?: RefreshEnvironmentsOptions) { - await discoveryApi.triggerRefresh(undefined, options ? { clearCache: options.clearCache } : undefined); - 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, }, }; 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..229f37e1d087 --- /dev/null +++ b/src/client/proposedApiTypes.ts @@ -0,0 +1,265 @@ +// 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 environments: { + /** + * 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. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * 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. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * 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 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. + */ + 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 itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + }; +} + +export type RefreshOptions = { + /** + * 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; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * 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 ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * 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 EnvironmentPath = { + /** + * 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/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/locator.ts b/src/client/pythonEnvironments/base/locator.ts index 609010501d63..687348964891 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. */ @@ -205,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/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/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index 7c12faf524c4..a8820a0f82b8 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -2,8 +2,8 @@ // 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'; import { PythonEnvInfo } from '../../info'; import { areSameEnv, getEnvPath } from '../../info/env'; @@ -52,13 +52,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 +75,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)) @@ -96,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' }, - ]); }); } @@ -115,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(); } @@ -194,6 +199,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..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); @@ -108,15 +111,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 +176,8 @@ export class EnvsCollectionService extends PythonEnvsWatcher, 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/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c41c52510280..44a69019601c 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, @@ -17,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'; @@ -26,6 +33,8 @@ 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'; +import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -62,11 +71,28 @@ 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 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. @@ -131,6 +157,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 +188,7 @@ async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise

{ 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 df9275c0eee4..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,33 +90,17 @@ 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(); + this.activate().ignoreErrors(); } 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; } @@ -101,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); @@ -115,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 @@ -134,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/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 { 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/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/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..b2dc97882e23 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +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'; + +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; + token?: CancellationToken; +} + +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), + { + placeHolder: CreateEnv.pickWorkspacePlaceholder, + ignoreFocusOut: true, + canPickMany: options?.allowMultiSelect, + }, + options?.token, + ); + + 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..76263dd9315a --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, Disposable } from 'vscode'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry, IInterpreterPathService } from '../../common/types'; +import { registerCommand } from '../../common/vscodeApis/commandApis'; +import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; +import { condaCreationProvider } from './provider/condaCreationProvider'; +import { VenvCreationProvider } from './provider/venvCreationProvider'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; + +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 const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreatingEnvironment } = getCreationEvents(); + +export function registerCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + interpreterPathService: IInterpreterPathService, +): void { + disposables.push( + registerCommand( + Commands.Create_Environment, + (options?: CreateEnvironmentOptions): Promise => { + const providers = _createEnvironmentProviders.getAll(); + return handleCreateEnvironmentCommand(providers, options); + }, + ), + ); + 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/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts new file mode 100644 index 000000000000..7489da89f123 --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Event, EventEmitter, QuickPickItem } from 'vscode'; +import { CreateEnv } from '../../common/utils/localize'; +import { showQuickPick } from '../../common/vscodeApis/windowApis'; +import { traceError } from '../../logging'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; + +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 { + 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 new file mode 100644 index 000000000000..3845a9ce8dad --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; +import { traceError, traceLog } from '../../../logging'; +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 { Common, CreateEnv } from '../../../common/utils/localize'; +import { getConda, pickPythonVersion } from './condaUtils'; +import { showErrorMessageWithLogs } from '../common/commonUtils'; +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; + 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; +} + +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, + token?: CancellationToken, +): Promise { + progress.report({ + message: CreateEnv.Conda.creating, + }); + + 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, + }, + }); + + 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) || 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); + }, + () => { + 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): Promise { + const conda = await getConda(); + if (!conda) { + return undefined; + } + + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; + if (!workspace) { + traceError('Workspace was not selected or found for creating virtual env.'); + return undefined; + } + + const version = await pickPythonVersion(); + if (!version) { + traceError('Conda environments for use with python extension require Python.'); + return undefined; + } + + 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 { + return { + createEnvironment, + name: 'Conda', + + description: CreateEnv.Conda.providerDescription, + + id: `${PVSC_EXTENSION_ID}:conda`, + }; +} 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 new file mode 100644 index 000000000000..256bdb6b01fb --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, 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(token?: CancellationToken): Promise { + const items: QuickPickItem[] = ['3.10', '3.9', '3.8', '3.7'].map((v) => ({ + label: `Python`, + description: v, + })); + const version = await showQuickPick( + items, + { + placeHolder: CreateEnv.Conda.selectPythonQuickPickPlaceholder, + }, + token, + ); + 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..8d9a677dc3f2 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 { Common, CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceLog } from '../../../logging'; +import { + CreateEnvironmentOptions, + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; +import { EnvironmentType, PythonEnvironment } from '../../info'; +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; + 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; +} + +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, + token?: CancellationToken, +): Promise { + 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, { + mergeStdOutErr: true, + token, + 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(os.EOL); + traceLog(output); + 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); + deferred.reject(error); + }, + () => { + dispose(); + if (!deferred.rejected) { + deferred.resolve(venvPath); + } + }, + ); + return deferred.promise; +} + +export class VenvCreationProvider implements CreateEnvironmentProvider { + 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; + } + + const interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [EnvironmentType.System, EnvironmentType.MicrosoftStore, EnvironmentType.Global].includes(i.envType), + ); + + if (interpreter) { + 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.'); + return undefined; + } + + name = 'Venv'; + + description: string = CreateEnv.Venv.providerDescription; + + id = `${PVSC_EXTENSION_ID}:venv`; +} 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 new file mode 100644 index 000000000000..6c844b8cfd02 --- /dev/null +++ b/src/client/pythonEnvironments/creation/types.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Progress, Uri } from 'vscode'; + +export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} + +export interface CreateEnvironmentOptions { + installPackages?: boolean; + ignoreSourceControl?: boolean; +} + +export interface CreateEnvironmentResult { + path: string | undefined; + uri: Uri | undefined; +} + +export interface CreateEnvironmentProvider { + createEnvironment(options?: CreateEnvironmentOptions): Promise; + name: string; + description: string; + id: string; +} 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/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 2ab6c8a8a3ba..7f6f8b9c58a3 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', @@ -99,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 7f056894f79e..4e4e9ba39649 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1107,6 +1107,27 @@ 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"}, + "apiName" : { "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 API called. + */ + apiName: string; + }; /** * Telemetry event sent with details after updating the python interpreter */ @@ -1388,7 +1409,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 +1420,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 +1431,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 +1442,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 +1453,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 +1462,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 +1473,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 +1484,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 +1496,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 +1504,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 +1545,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 +1553,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 +1633,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 +1654,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 +1670,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 +1692,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 +1700,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 +1718,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; /* @@ -1785,7 +1806,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 +1823,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 +1842,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 +1859,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 +1870,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 +1887,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 +1900,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 +1910,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 +1922,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 +1932,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 +1952,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 +1961,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,14 +1971,100 @@ 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; + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; + /** + * Telemetry event sent before creating an environment. + */ + /* __GDPR__ + "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__ - "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" } - } - */ + "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/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index ce4723992c86..c7be1af23fa6 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -301,14 +301,22 @@ "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" }, + "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ 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, 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/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); }); 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', () => { 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/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)); }); 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/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index abf56e1aca41..b42b1aaa94e7 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -16,7 +16,7 @@ import { WorkspaceFolder, } from 'vscode'; import { cloneDeep } from 'lodash'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything, instance, mock, when, verify } from 'ts-mockito'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; import { PathUtils } from '../../../../client/common/platform/pathUtils'; import { IPlatformService } from '../../../../client/common/platform/types'; @@ -80,6 +80,7 @@ suite('Set Interpreter Command', () => { 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'); @@ -243,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, @@ -266,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'); }); @@ -280,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, @@ -307,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'); }); @@ -466,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'; @@ -524,7 +644,6 @@ suite('Set Interpreter Command', () => { const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions, - activeItem: recommended, matchOnDetail: true, matchOnDescription: true, title: InterpreterQuickPickList.browsePath.openButtonLabel, @@ -548,6 +667,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'); }); @@ -568,17 +690,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/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 da8213bd1bd4..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,10 +53,10 @@ suite('Debugging - Configuration Provider Flask', () => { request: 'launch', module: 'flask', env: { - FLASK_APP: 'xyz.py', - FLASK_ENV: 'development', + FLASK_APP: 'app.py', + FLASK_DEBUG: '1', }, - args: ['run', '--no-debugger'], + args: ['run', '--no-debugger', '--no-reload'], jinja: true, justMyCode: true, }; @@ -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, @@ -87,9 +78,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, }; @@ -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, @@ -112,9 +101,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, }; 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, 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( 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/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 442ad7cdf3d6..dff756cd3e64 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -18,25 +18,19 @@ 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'; import { IInterpreterComparer, + IInterpreterQuickPick, IInterpreterSelector, 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'; @@ -53,8 +47,8 @@ suite('Interpreters - Service Registry', () => { [IExtensionSingleActivationService, InstallPythonCommand], [IExtensionSingleActivationService, InstallPythonViaTerminal], [IExtensionSingleActivationService, SetInterpreterCommand], + [IInterpreterQuickPick, SetInterpreterCommand], [IExtensionSingleActivationService, ResetInterpreterCommand], - [IExtensionSingleActivationService, SetShebangInterpreterCommand], [IExtensionActivationService, VirtualEnvironmentPrompt], @@ -63,9 +57,7 @@ suite('Interpreters - Service Registry', () => { [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], - [IInterpreterSelector, InterpreterSelector], - [IShebangCodeLensProvider, ShebangCodeLensProvider], [IInterpreterHelper, InterpreterHelper], [IInterpreterComparer, EnvironmentTypeComparer], 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/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', () => { 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/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index ad4cdc904a22..a4358c0830fc 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -3,404 +3,402 @@ 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 { normCasePath } from '../client/common/platform/fs-paths'; +import { + ActiveEnvironmentPathChangeEvent, + EnvironmentsChangeEvent, + ProposedExtensionAPI, +} from '../client/proposedApiTypes'; 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: ActiveEnvironmentPathChangeEvent[] = []; + proposed.environments.onDidChangeActiveEnvironmentPath((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('getActiveEnvironmentPath: 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.environments.getActiveEnvironmentPath(); + 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('getActiveEnvironmentPath: default python', () => { + const pythonPath = 'python'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environments.getActiveEnvironmentPath(); + assert.deepEqual(actual, { + id: 'DEFAULT_PYTHON', + path: pythonPath, + }); }); - test('getActiveInterpreterPath: No resource', async () => { + test('getActiveEnvironmentPath: 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.environments.getActiveEnvironmentPath(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.environments.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.environments.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.environments.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.environments.known; + 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.environments.known; + 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.environments.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('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()); - 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.environments.updateActiveEnvironmentPath('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('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()); - 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.environments.updateActiveEnvironmentPath({ + id: normCasePath('this/is/a/test/python/path'), + path: 'this/is/a/test/python/path', + }); + + interpreterPathService.verifyAll(); }); - test('setActiveInterpreter: no resource', async () => { + test('updateActiveEnvironmentPath: 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.environments.updateActiveEnvironmentPath('this/is/a/test/python/path', uri); interpreterPathService.verifyAll(); }); - test('setActiveInterpreter: with resource', async () => { - const resource = Uri.parse('a'); + + test('updateActiveEnvironmentPath: 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.environments.updateActiveEnvironmentPath('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.environments.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.environments.refreshEnvironments({ forceRefresh: true }); + + discoverAPI.verifyAll(); }); }); 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); - }); -}); 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..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'; @@ -54,8 +52,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 +70,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 +94,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 +110,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; }); @@ -124,7 +127,6 @@ suite('Python envs locator - Environments Collection', async () => { }, }); collectionService = new EnvsCollectionService(cache, parentLocator); - reportInterpretersChangedStub = sinon.stub(proposedApi, 'reportInterpretersChanged'); }); teardown(() => { @@ -145,99 +147,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 +221,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 +253,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 +351,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(); @@ -568,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 () => { @@ -597,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 () => { @@ -620,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 () => { @@ -659,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 () => { @@ -714,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); }); }); 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..4a480cfd6e44 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'; @@ -37,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; @@ -66,6 +68,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 +81,7 @@ suite('Python envs locator - Environments Resolver', () => { name = '', location = '', display: string | undefined = undefined, + type?: PythonEnvType, ): PythonEnvInfo { return { name, @@ -94,6 +100,7 @@ suite('Python envs locator - Environments Resolver', () => { distro: { org: '' }, searchLocation: Uri.file(location), source: [], + type, }; } suite('iterEnvs()', () => { @@ -109,7 +116,7 @@ suite('Python envs locator - Environments Resolver', () => { }); }), ); - sinon.stub(externalDependencies, 'getWorkspaceFolders').returns([testVirtualHomeDir]); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); }); teardown(() => { @@ -128,6 +135,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 +159,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); @@ -193,19 +203,21 @@ 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'), + undefined, + PythonEnvType.Virtual, ); const envsReturnedByParentLocator = [env]; const didUpdate = new EventEmitter | ProgressNotificationEvent>(); @@ -225,7 +237,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(); }); @@ -338,7 +350,7 @@ suite('Python envs locator - Environments Resolver', () => { }); }), ); - sinon.stub(externalDependencies, 'getWorkspaceFolders').returns([testVirtualHomeDir]); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); }); teardown(() => { @@ -355,6 +367,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..2310f6dc942f 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'; @@ -25,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([]); }); @@ -76,6 +78,7 @@ suite('Resolver Utils', () => { }, source: [], org: 'miniconda3', + type: PythonEnvType.Conda, }); envInfo.location = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12'); envInfo.name = 'base'; @@ -209,6 +212,7 @@ suite('Resolver Utils', () => { version: UNKNOWN_PYTHON_VERSION, fileInfo: undefined, name: 'base', + type: PythonEnvType.Conda, }); setEnvDisplayString(info); return info; @@ -237,6 +241,7 @@ suite('Resolver Utils', () => { searchLocation: undefined, source: [], }; + info.type = PythonEnvType.Conda; setEnvDisplayString(info); return info; } @@ -333,6 +338,7 @@ suite('Resolver Utils', () => { distro: { org: '' }, searchLocation: Uri.file(location), source: [], + type: PythonEnvType.Virtual, }; setEnvDisplayString(info); return info; @@ -623,6 +629,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'; 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'); + }); +}); 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/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts new file mode 100644 index 000000000000..9c8e1af42b9a --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -0,0 +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 * as windowApis from '../../../client/common/vscodeApis/windowApis'; +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 showQuickPickStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let startedEventTriggered = false; + let exitedEventTriggered = false; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + startedEventTriggered = false; + exitedEventTriggered = false; + disposables.push( + onCreateEnvironmentStarted(() => { + startedEventTriggered = true; + }), + ); + disposables.push( + onCreateEnvironmentStarted(() => { + exitedEventTriggered = true; + }), + ); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Successful environment creation', 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); + + 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.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(handleCreateEnvironmentCommand([provider.object])); + + 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 new file mode 100644 index 000000000000..65b5affcbf8f --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -0,0 +1,186 @@ +// 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 * as typemoq from 'typemoq'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; +import { + 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(() => { + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + 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(); + }); + + 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'); + 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(); + 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, + }; + }); + + 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); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + assert.deepStrictEqual(await promise, { path: 'new_environment', uri: workspace1.uri }); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + 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, + }; + }); + + 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 new file mode 100644 index 000000000000..1fb959f228ea --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -0,0 +1,191 @@ +// 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 { CancellationToken, ProgressOptions, Uri } from 'vscode'; +import { + 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 { 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); + +suite('venv Creation provider tests', () => { + let venvProvider: CreateEnvironmentProvider; + let pickWorkspaceFolderStub: sinon.SinonStub; + 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'); + interpreterQuickPick = typemoq.Mock.ofType(); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + progressMock = typemoq.Mock.ofType(); + venvProvider = new VenvCreationProvider(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, + }); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + assert.isUndefined(await venvProvider.createEnvironment()); + interpreterQuickPick.verifyAll(); + }); + + 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, + }; + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .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, + }; + }); + + 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(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + 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 () => { + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + + interpreterQuickPick + .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); + 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, + }; + }); + + 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); + }); +}); 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) => 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',