diff --git a/extensions/positron-python/.github/actions/build-vsix/action.yml b/extensions/positron-python/.github/actions/build-vsix/action.yml index ae7b8fddba6..b632eb0a25c 100644 --- a/extensions/positron-python/.github/actions/build-vsix/action.yml +++ b/extensions/positron-python/.github/actions/build-vsix/action.yml @@ -16,7 +16,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node_version }} cache: 'npm' diff --git a/extensions/positron-python/.github/actions/lint/action.yml b/extensions/positron-python/.github/actions/lint/action.yml index 1d302b055be..1e4fd0712f7 100644 --- a/extensions/positron-python/.github/actions/lint/action.yml +++ b/extensions/positron-python/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node_version }} cache: 'npm' diff --git a/extensions/positron-python/.github/dependabot.yml b/extensions/positron-python/.github/dependabot.yml index de5ebfe9158..14c8e18d475 100644 --- a/extensions/positron-python/.github/dependabot.yml +++ b/extensions/positron-python/.github/dependabot.yml @@ -37,7 +37,6 @@ updates: - dependency-name: prospector # Due to Python 2.7 and #14477. - dependency-name: pytest # Due to Python 2.7 and #13776. - dependency-name: py # Due to Python 2.7. - - dependency-name: isort - dependency-name: jedi-language-server labels: - 'no-changelog' diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index 56d9c04f0cd..d17d5fff4b9 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: path: ${{ env.special-working-directory-relative }} - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index 9229393ce5c..4ade4fd2af1 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -143,7 +143,7 @@ jobs: path: ${{ env.special-working-directory-relative }} - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -356,7 +356,7 @@ jobs: uses: actions/checkout@v4 - name: Install Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' diff --git a/extensions/positron-python/.vscode/extensions.json b/extensions/positron-python/.vscode/extensions.json index 5ade8dec488..93a73827e7a 100644 --- a/extensions/positron-python/.vscode/extensions.json +++ b/extensions/positron-python/.vscode/extensions.json @@ -7,6 +7,8 @@ "dbaeumer.vscode-eslint", "ms-python.python", "ms-python.black-formatter", - "ms-python.vscode-pylance" + "ms-python.vscode-pylance", + "ms-python.isort", + "ms-python.flake8" ] } diff --git a/extensions/positron-python/.vscode/settings.json b/extensions/positron-python/.vscode/settings.json index 86b34bfd81d..eaba16a0ac4 100644 --- a/extensions/positron-python/.vscode/settings.json +++ b/extensions/positron-python/.vscode/settings.json @@ -46,12 +46,8 @@ "editor.formatOnSave": true }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "python.linting.enabled": false, - "python.formatting.provider": "black", - "python.sortImports.args": ["--profile", "black"], "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", - "typescriptHero.imports.stringQuoteStyle": "'", "prettier.printWidth": 120, "prettier.singleQuote": true, "editor.codeActionsOnSave": { @@ -72,12 +68,16 @@ "pythonFiles/tests" ], "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false, // Branch name suggestion. "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "git.branchRandomName.enable": true, "git.branchProtection": ["main", "release/*"], "git.pullBeforeCheckout": true, // Open merge editor for resolving conflicts. - "git.mergeEditor": true + "git.mergeEditor": true, + "python.testing.pytestArgs": [ + "pythonFiles/tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } diff --git a/extensions/positron-python/README.md b/extensions/positron-python/README.md index 0a8766f086a..8029aa09658 100644 --- a/extensions/positron-python/README.md +++ b/extensions/positron-python/README.md @@ -60,7 +60,7 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | | `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | +| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | To see all available Python commands, open the Command Palette and type `Python`. For Jupyter extension commands, just type `Jupyter`. @@ -71,16 +71,11 @@ Learn more about the rich features of the Python extension: - [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more - [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more -- [Code formatting](https://code.visualstudio.com/docs/python/editing#_formatting): Format your code with black, autopep or yapf - +- [Code formatting](https://code.visualstudio.com/docs/python/formatting): Format your code with black, autopep or yapf - [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes - - [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest or pytest. - - [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more - - [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments - - [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). diff --git a/extensions/positron-python/ThirdPartyNotices-Repository.txt b/extensions/positron-python/ThirdPartyNotices-Repository.txt index c8854a208e5..9e7e822af1b 100644 --- a/extensions/positron-python/ThirdPartyNotices-Repository.txt +++ b/extensions/positron-python/ThirdPartyNotices-Repository.txt @@ -6,18 +6,17 @@ Microsoft Python extension for Visual Studio Code incorporates third party mater 1. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) 2. Files from the Python Project (https://www.python.org/) -3. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) -4. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) -5. PTVS (https://github.com/Microsoft/PTVS) -6. Python documentation (https://docs.python.org/) -7. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) -8. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) -9. Sphinx (http://sphinx-doc.org/) -10. nteract (https://github.com/nteract/nteract) -11. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) -12. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) -13. mocha (https://github.com/mochajs/mocha) -14. get-pip (https://github.com/pypa/get-pip) +3. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +4. PTVS (https://github.com/Microsoft/PTVS) +5. Python documentation (https://docs.python.org/) +6. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +7. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +8. Sphinx (http://sphinx-doc.org/) +9. nteract (https://github.com/nteract/nteract) +10. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) +11. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) +12. mocha (https://github.com/mochajs/mocha) +13. get-pip (https://github.com/pypa/get-pip) %% Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE @@ -244,25 +243,6 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF Files from the Python Project NOTICES, INFORMATION, AND LICENSE -%% Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE BEGIN HERE -========================================= - * Copyright 2006 Google Inc. - * http://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -========================================= -END OF Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE - %% omnisharp-vscode NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright (c) Microsoft Corporation diff --git a/extensions/positron-python/build/test-requirements.txt b/extensions/positron-python/build/test-requirements.txt index a489e7ae742..d2c24b6bfbd 100644 --- a/extensions/positron-python/build/test-requirements.txt +++ b/extensions/positron-python/build/test-requirements.txt @@ -1,13 +1,8 @@ # pin setoptconf to prevent issue with 'use_2to3' setoptconf==0.3.0 -# Install flake8 first, as both flake8 and autopep8 require pycodestyle, -# but flake8 has a tighter pinning. flake8 -autopep8 bandit -black -yapf pylint pycodestyle pydocstyle @@ -18,7 +13,6 @@ flask fastapi uvicorn django -isort # Integrated TensorBoard tests tensorboard diff --git a/extensions/positron-python/build/webpack/common.js b/extensions/positron-python/build/webpack/common.js index b248b29fdd6..fca1b1a900f 100644 --- a/extensions/positron-python/build/webpack/common.js +++ b/extensions/positron-python/build/webpack/common.js @@ -21,7 +21,6 @@ exports.nodeModulesToExternalize = [ 'unicode/category/Nd', 'unicode/category/Pc', 'source-map-support', - 'diff-match-patch', 'sudo-prompt', 'node-stream-zip', 'xml2js', diff --git a/extensions/positron-python/build/webpack/webpack.extension.config.js b/extensions/positron-python/build/webpack/webpack.extension.config.js index 79a6556d708..9c1188d53fe 100644 --- a/extensions/positron-python/build/webpack/webpack.extension.config.js +++ b/extensions/positron-python/build/webpack/webpack.extension.config.js @@ -72,6 +72,7 @@ const config = { resolve: { extensions: ['.ts', '.js'], plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], + conditionNames: ['import', 'require', 'node'], }, output: { filename: '[name].js', diff --git a/extensions/positron-python/gulpfile.js b/extensions/positron-python/gulpfile.js index d5579b381d3..656adc09ae0 100644 --- a/extensions/positron-python/gulpfile.js +++ b/extensions/positron-python/gulpfile.js @@ -20,7 +20,9 @@ const nativeDependencyChecker = require('node-has-native-dependencies'); const flat = require('flat'); const { argv } = require('yargs'); const os = require('os'); +// --- Start Positron --- const rmrf = require('rimraf'); +// --- End Positron --- const typescript = require('typescript'); const tsProject = ts.createProject('./tsconfig.json', { typescript }); @@ -247,6 +249,7 @@ gulp.task('prePublishBundle', gulp.series('webpack', 'renameSourceMaps')); gulp.task('checkDependencies', gulp.series('checkNativeDependencies')); gulp.task('prePublishNonBundle', gulp.series('compile')); +// --- Start Positron --- gulp.task('installPythonRequirements', async () => { let args = [ '-m', @@ -264,9 +267,7 @@ gulp.task('installPythonRequirements', async () => { '-r', './requirements.txt', ]; - // --- Start Positron --- await spawnAsync(pythonCommand, args, undefined, true) - // --- End Positron --- .then(() => true) .catch((ex) => { console.error("Failed to install requirements using 'python'", ex); @@ -289,9 +290,7 @@ gulp.task('installPythonRequirements', async () => { '-r', './pythonFiles/jedilsp_requirements/requirements.txt', ]; - // --- Start Positron --- await spawnAsync(pythonCommand, args, undefined, true) - // --- End Positron --- .then(() => true) .catch((ex) => { console.error("Failed to install Jedi LSP requirements using 'python'", ex); @@ -313,9 +312,7 @@ gulp.task('installDebugpy', async () => { '-r', './build/build-install-requirements.txt', ]; - // --- Start Positron --- await spawnAsync(pythonCommand, depsArgs, undefined, true) - // --- End Positron --- .then(() => true) .catch((ex) => { console.error("Failed to install dependencies need by 'install_debugpy.py' using 'python'", ex); @@ -325,9 +322,7 @@ gulp.task('installDebugpy', async () => { // Install new DEBUGPY with wheels for python const wheelsArgs = ['./pythonFiles/install_debugpy.py']; const wheelsEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; - // --- Start Positron --- await spawnAsync(pythonCommand, wheelsArgs, wheelsEnv, true) - // --- End Positron --- .then(() => true) .catch((ex) => { console.error("Failed to install DEBUGPY wheels using 'python'", ex); @@ -349,7 +344,6 @@ gulp.task('installDebugpy', async () => { gulp.task('installPythonLibs', gulp.series('installPythonRequirements', 'installDebugpy')); -// --- Start Positron --- function locatePython() { let pythonPath = process.env.CI_PYTHON_PATH || 'python3'; const whichCommand = os.platform() === 'win32' ? 'where' : 'which'; diff --git a/extensions/positron-python/noxfile.py b/extensions/positron-python/noxfile.py new file mode 100644 index 00000000000..b9ebba64544 --- /dev/null +++ b/extensions/positron-python/noxfile.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pathlib +import nox +import shutil + + +@nox.session() +def install_python_libs(session: nox.Session): + requirements = [ + ("./pythonFiles/lib/python", "./requirements.txt"), + ( + "./pythonFiles/lib/jedilsp", + "./pythonFiles/jedilsp_requirements/requirements.txt", + ), + ] + for target, file in requirements: + session.install( + "-t", + target, + "--no-cache-dir", + "--implementation", + "py", + "--no-deps", + "--require-hashes", + "--only-binary", + ":all:", + "-r", + file, + ) + + session.install("packaging") + + # Install debugger + session.run( + "python", + "./pythonFiles/install_debugpy.py", + env={"PYTHONPATH": "./pythonFiles/lib/temp"}, + ) + + # Download get-pip script + session.run( + "python", + "./pythonFiles/download_get_pip.py", + env={"PYTHONPATH": "./pythonFiles/lib/temp"}, + ) + + if pathlib.Path("./pythonFiles/lib/temp").exists(): + shutil.rmtree("./pythonFiles/lib/temp") diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index a08ca84e734..46fa5a4dc6e 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -22,7 +22,8 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor" + "saveEditor", + "terminalDataWriteEvent" ], "author": { "name": "Microsoft Corporation" @@ -526,14 +527,18 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", - "pythonTestAdapter" + "pythonTestAdapter", + "pythonREPLSmartSend", + "pythonRecommendTensorboardExt" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", - "%python.experiments.pythonTestAdapter.description%" + "%python.experiments.pythonTestAdapter.description%", + "%python.experiments.pythonREPLSmartSend.description%", + "%python.experiments.pythonRecommendTensorboardExt.description%" ] }, "scope": "machine", @@ -549,91 +554,22 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", - "pythonTestAdapter" + "pythonTestAdapter", + "pythonREPLSmartSend" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", - "%python.experiments.pythonTestAdapter.description%" + "%python.experiments.pythonTestAdapter.description%", + "%python.experiments.pythonREPLSmartSend.description%" ] }, "scope": "machine", "type": "array", "uniqueItems": true }, - "python.formatting.autopep8Args": { - "default": [], - "description": "%python.formatting.autopep8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.autopep8Args.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.autopep8Args.deprecationMessage%" - }, - "python.formatting.autopep8Path": { - "default": "autopep8", - "description": "%python.formatting.autopep8Path.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.autopep8Path.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.autopep8Path.deprecationMessage%" - }, - "python.formatting.blackArgs": { - "default": [], - "description": "%python.formatting.blackArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.blackArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.blackArgs.deprecationMessage%" - }, - "python.formatting.blackPath": { - "default": "black", - "description": "%python.formatting.blackPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.blackPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.blackPath.deprecationMessage%" - }, - "python.formatting.provider": { - "default": "autopep8", - "description": "%python.formatting.provider.description%", - "enum": [ - "autopep8", - "black", - "none", - "yapf" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.provider.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.provider.deprecationMessage%" - }, - "python.formatting.yapfArgs": { - "default": [], - "description": "%python.formatting.yapfArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.yapfArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.yapfArgs.deprecationMessage%" - }, - "python.formatting.yapfPath": { - "default": "yapf", - "description": "%python.formatting.yapfPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.yapfPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.yapfPath.deprecationMessage%" - }, "python.globalModuleInstallation": { "default": false, "description": "%python.globalModuleInstallation.description%", @@ -659,88 +595,6 @@ "scope": "application", "type": "string" }, - "python.linting.banditArgs": { - "default": [], - "description": "%python.linting.banditArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" - }, - "python.linting.banditEnabled": { - "default": false, - "description": "%python.linting.banditEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" - }, - "python.linting.banditPath": { - "default": "bandit", - "description": "%python.linting.banditPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.banditPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.banditPath.deprecationMessage%" - }, - "python.linting.cwd": { - "default": null, - "description": "%python.linting.cwd.description%", - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.cwd.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.cwd.deprecationMessage%" - }, - "python.linting.enabled": { - "default": true, - "description": "%python.linting.enabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.enabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.enabled.deprecationMessage%" - }, - "python.linting.flake8Args": { - "default": [], - "description": "%python.linting.flake8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.flake8Args.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8Args.deprecationMessage%" - }, - "python.linting.flake8CategorySeverity.E": { - "default": "Error", - "description": "%python.linting.flake8CategorySeverity.E.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.E.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8CategorySeverity.E.deprecationMessage%" - }, - "python.linting.flake8CategorySeverity.F": { - "default": "Error", - "description": "%python.linting.flake8CategorySeverity.F.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.F.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8CategorySeverity.F.deprecationMessage%" - }, "python.interpreter.infoVisibility": { "default": "never", "description": "%python.interpreter.infoVisibility.description%", @@ -757,360 +611,6 @@ "scope": "machine", "type": "string" }, - "python.linting.flake8CategorySeverity.W": { - "default": "Warning", - "description": "%python.linting.flake8CategorySeverity.W.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.W.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8CategorySeverity.W.deprecationMessage%" - }, - "python.linting.flake8Enabled": { - "default": false, - "description": "%python.linting.flake8Enabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.flake8Enabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8Enabled.deprecationMessage%" - }, - "python.linting.flake8Path": { - "default": "flake8", - "description": "%python.linting.flake8Path.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8Path.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8Path.deprecationMessage%" - }, - "python.linting.ignorePatterns": { - "default": [ - "**/site-packages/**/*.py", - ".vscode/*.py" - ], - "description": "%python.linting.ignorePatterns.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "uniqueItems": true, - "markdownDeprecationMessage": "%python.linting.ignorePatterns.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.ignorePatterns.deprecationMessage%" - }, - "python.linting.lintOnSave": { - "default": true, - "description": "%python.linting.lintOnSave.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.lintOnSave.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.lintOnSave.deprecationMessage%" - }, - "python.linting.maxNumberOfProblems": { - "default": 100, - "description": "%python.linting.maxNumberOfProblems.description%", - "scope": "resource", - "type": "number", - "markdownDeprecationMessage": "%python.linting.maxNumberOfProblems.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.maxNumberOfProblems.deprecationMessage%" - }, - "python.linting.mypyArgs": { - "default": [ - "--follow-imports=silent", - "--ignore-missing-imports", - "--show-column-numbers", - "--no-pretty" - ], - "description": "%python.linting.mypyArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.mypyArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyArgs.deprecationMessage%" - }, - "python.linting.mypyCategorySeverity.error": { - "default": "Error", - "description": "%python.linting.mypyCategorySeverity.error.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.error.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyCategorySeverity.error.deprecationMessage%" - }, - "python.linting.mypyCategorySeverity.note": { - "default": "Information", - "description": "%python.linting.mypyCategorySeverity.note.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.note.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyCategorySeverity.note.deprecationMessage%" - }, - "python.linting.mypyEnabled": { - "default": false, - "description": "%python.linting.mypyEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.mypyEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyEnabled.deprecationMessage%" - }, - "python.linting.mypyPath": { - "default": "mypy", - "description": "%python.linting.mypyPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.mypyPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyPath.deprecationMessage%" - }, - "python.linting.prospectorArgs": { - "default": [], - "description": "%python.linting.prospectorArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.prospectorArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.prospectorArgs.deprecationMessage%" - }, - "python.linting.prospectorEnabled": { - "default": false, - "description": "%python.linting.prospectorEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.prospectorEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.prospectorEnabled.deprecationMessage%" - }, - "python.linting.prospectorPath": { - "default": "prospector", - "description": "%python.linting.prospectorPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.prospectorPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.prospectorPath.deprecationMessage%" - }, - "python.linting.pycodestyleArgs": { - "default": [], - "description": "%python.linting.pycodestyleArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pycodestyleArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleArgs.deprecationMessage%" - }, - "python.linting.pycodestyleCategorySeverity.E": { - "default": "Error", - "description": "%python.linting.pycodestyleCategorySeverity.E.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.deprecationMessage%" - }, - "python.linting.pycodestyleCategorySeverity.W": { - "default": "Warning", - "description": "%python.linting.pycodestyleCategorySeverity.W.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.deprecationMessage%" - }, - "python.linting.pycodestyleEnabled": { - "default": false, - "description": "%python.linting.pycodestyleEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pycodestyleEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleEnabled.deprecationMessage%" - }, - "python.linting.pycodestylePath": { - "default": "pycodestyle", - "description": "%python.linting.pycodestylePath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pycodestylePath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestylePath.deprecationMessage%" - }, - "python.linting.pydocstyleArgs": { - "default": [], - "description": "%python.linting.pydocstyleArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pydocstyleArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pydocstyleArgs.deprecationMessage%" - }, - "python.linting.pydocstyleEnabled": { - "default": false, - "description": "%python.linting.pydocstyleEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pydocstyleEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pydocstyleEnabled.deprecationMessage%" - }, - "python.linting.pydocstylePath": { - "default": "pydocstyle", - "description": "%python.linting.pydocstylePath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pydocstylePath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pydocstylePath.deprecationMessage%" - }, - "python.linting.pylamaArgs": { - "default": [], - "description": "%python.linting.pylamaArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pylamaArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylamaArgs.deprecationMessage%" - }, - "python.linting.pylamaEnabled": { - "default": false, - "description": "%python.linting.pylamaEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pylamaEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylamaEnabled.deprecationMessage%" - }, - "python.linting.pylamaPath": { - "default": "pylama", - "description": "%python.linting.pylamaPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylamaPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylamaPath.deprecationMessage%" - }, - "python.linting.pylintArgs": { - "default": [], - "description": "%python.linting.pylintArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pylintArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintArgs.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.convention": { - "default": "Information", - "description": "%python.linting.pylintCategorySeverity.convention.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.convention.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.error": { - "default": "Error", - "description": "%python.linting.pylintCategorySeverity.error.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.error.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.error.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.fatal": { - "default": "Error", - "description": "%python.linting.pylintCategorySeverity.fatal.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.fatal.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.refactor": { - "default": "Hint", - "description": "%python.linting.pylintCategorySeverity.refactor.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.refactor.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.warning": { - "default": "Warning", - "description": "%python.linting.pylintCategorySeverity.warning.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.warning.deprecationMessage%" - }, - "python.linting.pylintEnabled": { - "default": false, - "description": "%python.linting.pylintEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pylintEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintEnabled.deprecationMessage%" - }, - "python.linting.pylintPath": { - "default": "pylint", - "description": "%python.linting.pylintPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintPath.deprecationMessage%" - }, "python.logging.level": { "default": "error", "deprecationMessage": "%python.logging.level.deprecation%", @@ -1152,28 +652,13 @@ "scope": "machine-overridable", "type": "string" }, - "python.sortImports.args": { - "default": [], - "description": "%python.sortImports.args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "deprecationMessage": "%python.sortImports.args.deprecationMessage%" - }, - "python.sortImports.path": { - "default": "", - "description": "%python.sortImports.path.description%", - "scope": "machine-overridable", - "type": "string", - "deprecationMessage": "%python.sortImports.path.deprecationMessage%" - }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.tensorBoard.logDirectory.markdownDeprecationMessage%", + "deprecationMessage": "%python.tensorBoard.logDirectory.deprecationMessage%" }, "python.terminal.activateEnvInCurrentTerminal": { "default": false, @@ -1845,7 +1330,7 @@ "category": "Python", "command": "python.launchTensorBoard", "title": "%python.command.python.launchTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", @@ -1853,7 +1338,7 @@ "enablement": "python.hasActiveTensorBoardSession", "icon": "$(refresh)", "title": "%python.command.python.refreshTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", @@ -2089,7 +1574,6 @@ "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", - "diff-match-patch": "^1.0.0", "fs-extra": "^10.0.1", "glob": "^7.2.0", "hash.js": "^1.1.7", @@ -2097,7 +1581,6 @@ "inversify": "^6.0.1", "jsonc-parser": "^3.0.0", "lodash": "^4.17.21", - "md5": "^2.2.1", "minimatch": "^5.0.1", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", @@ -2113,12 +1596,10 @@ "uint64be": "^3.0.0", "unicode": "^14.0.0", "untildify": "^4.0.0", - "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "^8.1.0", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-protocol": "^3.17.3", + "vscode-jsonrpc": "^8.2.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -2130,21 +1611,17 @@ "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", - "@types/diff-match-patch": "^1.0.32", "@types/download": "^8.0.1", "@types/fs-extra": "^9.0.13", "@types/glob": "^7.2.0", "@types/lodash": "^4.14.104", - "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", - "@types/nock": "^10.0.3", "@types/node": "^18.17.1", "@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.81.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", @@ -2161,7 +1638,6 @@ "cross-spawn": "^6.0.5", "del": "^6.0.0", "download": "^8.0.0", - "es5-ext": "0.10.53", "eslint": "^7.2.0", "eslint-config-airbnb": "^18.2.0", "eslint-config-prettier": "^8.3.0", @@ -2177,7 +1653,7 @@ "mocha": "^9.2.2", "mocha-junit-reporter": "^2.0.2", "mocha-multi-reporters": "^1.1.7", - "nock": "^10.0.6", + "node-has-native-dependencies": "^1.0.2", "node-loader": "^1.0.2", "node-polyfill-webpack-plugin": "^1.1.4", @@ -2195,7 +1671,6 @@ "typemoq": "^2.1.0", "typescript": "4.5.5", "uuid": "^8.3.2", - "vscode-debugadapter-testsupport": "^1.27.0", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index 0160d70e452..65437b33cef 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -1,7 +1,6 @@ { "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "python.command.python.sortImports.title": "Sort Imports", "python.command.python.startREPL.title": "Start REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", @@ -45,27 +44,8 @@ "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", - "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "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.autopep8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.autopep8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.blackArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.blackArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", - "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", - "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", + "python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServerDebug.description": "Whether debug should be enabled for Positron's Python language server.", "python.languageServerLogLevel.description": "Controls the [logging level](https://docs.python.org/3/library/logging.html#levels) of Positron's Python language server. Requires a restart to take effect.", @@ -74,141 +54,18 @@ "python.languageServer.jediDescription": "Use Jedi behind the Language Server Protocol (LSP) as a language server.", "python.languageServer.pylanceDescription": "Use Pylance as a language server.", "python.languageServer.noneDescription": "Disable language server capabilities.", - "python.linting.banditArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.banditArgs.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.banditArgs.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.banditEnabled.description": "Whether to lint Python files using bandit.", - "python.linting.banditEnabled.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.banditEnabled.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.banditPath.description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", - "python.linting.banditPath.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.banditPath.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.cwd.description": "Optional working directory for linters.", - "python.linting.cwd.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.cwd.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.enabled.description": "Whether to lint Python files.", - "python.linting.enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.enabled.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.flake8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8CategorySeverity.E.description": "Severity of Flake8 message type 'E'.", - "python.linting.flake8CategorySeverity.E.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8CategorySeverity.E.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8CategorySeverity.F.description": "Severity of Flake8 message type 'F'.", - "python.linting.flake8CategorySeverity.F.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8CategorySeverity.F.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8CategorySeverity.W.description": "Severity of Flake8 message type 'W'.", - "python.linting.flake8CategorySeverity.W.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8CategorySeverity.W.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8Enabled.description": "Whether to lint Python files using flake8.", - "python.linting.flake8Enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8Enabled.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8Path.description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", - "python.linting.flake8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.ignorePatterns.description": "Patterns used to exclude files or folders from being linted.", - "python.linting.ignorePatterns.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.ignorePatterns.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.interpreter.infoVisibility.description": "Controls when to display information of selected interpreter in the status bar.", "python.interpreter.infoVisibility.never.description": "Never display information.", "python.interpreter.infoVisibility.onPythonRelated.description": "Only display information if Python-related files are opened.", "python.interpreter.infoVisibility.always.description": "Always display information.", - "python.linting.lintOnSave.description": "Whether to lint Python files when saved.", - "python.linting.lintOnSave.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.lintOnSave.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.maxNumberOfProblems.description": "Controls the maximum number of problems produced by the server.", - "python.linting.maxNumberOfProblems.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.maxNumberOfProblems.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.mypyArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyCategorySeverity.error.description": "Severity of Mypy message type 'Error'.", - "python.linting.mypyCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyCategorySeverity.note.description": "Severity of Mypy message type 'Note'.", - "python.linting.mypyCategorySeverity.note.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyCategorySeverity.note.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyEnabled.description": "Whether to lint Python files using mypy.", - "python.linting.mypyEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyPath.description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", - "python.linting.mypyPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyPath.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.prospectorArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.prospectorArgs.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.prospectorArgs.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.prospectorEnabled.description": "Whether to lint Python files using prospector.", - "python.linting.prospectorEnabled.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.prospectorEnabled.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.prospectorPath.description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", - "python.linting.prospectorPath.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.prospectorPath.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pycodestyleArgs.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleArgs.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleCategorySeverity.E.description": "Severity of pycodestyle message type 'E'.", - "python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleCategorySeverity.E.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleCategorySeverity.W.description": "Severity of pycodestyle message type 'W'.", - "python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleCategorySeverity.W.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleEnabled.description": "Whether to lint Python files using pycodestyle.", - "python.linting.pycodestyleEnabled.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleEnabled.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestylePath.description": "Path to pycodestyle, you can use a custom version of pycodestyle by modifying this setting to include the full path.", - "python.linting.pycodestylePath.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestylePath.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pydocstyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pydocstyleArgs.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pydocstyleArgs.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pydocstyleEnabled.description": "Whether to lint Python files using pydocstyle.", - "python.linting.pydocstyleEnabled.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pydocstyleEnabled.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pydocstylePath.description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", - "python.linting.pydocstylePath.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pydocstylePath.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylamaArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pylamaArgs.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylamaArgs.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylamaEnabled.description": "Whether to lint Python files using pylama.", - "python.linting.pylamaEnabled.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylamaEnabled.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylamaPath.description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", - "python.linting.pylamaPath.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylamaPath.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pylintArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.convention.description": "Severity of Pylint message type 'Convention/C'.", - "python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.convention.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.error.description": "Severity of Pylint message type 'Error/E'.", - "python.linting.pylintCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.fatal.description": "Severity of Pylint message type 'Error/F'.", - "python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.fatal.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.refactor.description": "Severity of Pylint message type 'Refactor/R'.", - "python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.refactor.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.warning.description": "Severity of Pylint message type 'Warning/W'.", - "python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.warning.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintEnabled.description": "Whether to lint Python files using pylint.", - "python.linting.pylintEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintPath.description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", - "python.linting.pylintPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", - "python.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.", + "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", + "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", @@ -225,8 +82,6 @@ "python.testing.unittestEnabled.description": "Enable testing using unittest.", "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", - "python.sortImports.args.deprecationMessage": "This setting will be removed soon. Use 'isort.args' instead.", - "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead.", "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", diff --git a/extensions/positron-python/pythonFiles/deactivate b/extensions/positron-python/pythonFiles/deactivate new file mode 100644 index 00000000000..6ede3da311a --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate @@ -0,0 +1,33 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${PS1:-}" +_OLD_VIRTUAL_PATH="$PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" +fi diff --git a/extensions/positron-python/pythonFiles/deactivate.csh b/extensions/positron-python/pythonFiles/deactivate.csh new file mode 100644 index 00000000000..ef4d0d39389 --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate.csh @@ -0,0 +1,6 @@ +# Same as deactivate in "/bin/activate.csh" +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Initialize the variables required by deactivate function +set _OLD_VIRTUAL_PROMPT="$prompt" +set _OLD_VIRTUAL_PATH="$PATH" diff --git a/extensions/positron-python/pythonFiles/deactivate.fish b/extensions/positron-python/pythonFiles/deactivate.fish new file mode 100644 index 00000000000..c652a8c1e3d --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate.fish @@ -0,0 +1,36 @@ +# Same as deactivate in "/bin/activate.fish" +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$vscode_python_old_fish_prompt_OVERRIDE" + set -e vscode_python_old_fish_prompt_OVERRIDE + if functions -q vscode_python_old_fish_prompt + functions -e fish_prompt + functions -c vscode_python_old_fish_prompt fish_prompt + functions -e vscode_python_old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + functions -e deactivate + end +end + +# Initialize the variables required by deactivate function +set -gx _OLD_VIRTUAL_PATH $PATH +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + functions -c fish_prompt vscode_python_old_fish_prompt +end +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME +end diff --git a/extensions/positron-python/pythonFiles/deactivate.ps1 b/extensions/positron-python/pythonFiles/deactivate.ps1 new file mode 100644 index 00000000000..65dd80907d9 --- /dev/null +++ b/extensions/positron-python/pythonFiles/deactivate.ps1 @@ -0,0 +1,31 @@ +# Same as deactivate in "Activate.ps1" +function global:deactivate ([switch]$NonDestructive) { + if (Test-Path function:_OLD_VIRTUAL_PROMPT) { + copy-item function:_OLD_VIRTUAL_PROMPT function:prompt + remove-item function:_OLD_VIRTUAL_PROMPT + } + if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) { + copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME + remove-item env:_OLD_VIRTUAL_PYTHONHOME + } + if (Test-Path env:_OLD_VIRTUAL_PATH) { + copy-item env:_OLD_VIRTUAL_PATH env:PATH + remove-item env:_OLD_VIRTUAL_PATH + } + if (Test-Path env:VIRTUAL_ENV) { + remove-item env:VIRTUAL_ENV + } + if (!$NonDestructive) { + remove-item function:deactivate + } +} + +# Initialize the variables required by deactivate function +if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { + function global:_OLD_VIRTUAL_PROMPT {""} + copy-item function:prompt function:_OLD_VIRTUAL_PROMPT +} +if (Test-Path env:PYTHONHOME) { + copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME +} +copy-item env:PATH env:_OLD_VIRTUAL_PATH diff --git a/extensions/positron-python/pythonFiles/normalizeSelection.py b/extensions/positron-python/pythonFiles/normalizeSelection.py index 0363702717a..7ace42daa90 100644 --- a/extensions/positron-python/pythonFiles/normalizeSelection.py +++ b/extensions/positron-python/pythonFiles/normalizeSelection.py @@ -6,6 +6,7 @@ import re import sys import textwrap +from typing import Iterable def split_lines(source): @@ -118,6 +119,8 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" + if selection[-2] == "}" or selection[-2] == "]": + source = source[:-1] except Exception: # If there's a problem when parsing statements, # append a blank line to end the block and send it as-is. @@ -126,17 +129,159 @@ def normalize_lines(selection): return source +top_level_nodes = [] +min_key = None + + +def check_exact_exist(top_level_nodes, start_line, end_line): + exact_nodes = [] + for node in top_level_nodes: + if node.lineno == start_line and node.end_lineno == end_line: + exact_nodes.append(node) + + return exact_nodes + + +def traverse_file(wholeFileContent, start_line, end_line, was_highlighted): + """ + Intended to traverse through a user's given file content and find, collect all appropriate lines + that should be sent to the REPL in case of smart selection. + This could be exact statement such as just a single line print statement, + or a multiline dictionary, or differently styled multi-line list comprehension, etc. + Then call the normalize_lines function to normalize our smartly selected code block. + """ + + parsed_file_content = ast.parse(wholeFileContent) + smart_code = "" + should_run_top_blocks = [] + + # Purpose of this loop is to fetch and collect all the + # AST top level nodes, and its node.body as child nodes. + # Individual nodes will contain information like + # the start line, end line and get source segment information + # that will be used to smartly select, and send normalized code. + for node in ast.iter_child_nodes(parsed_file_content): + top_level_nodes.append(node) + + ast_types_with_nodebody = ( + ast.Module, + ast.Interactive, + ast.Expression, + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.For, + ast.AsyncFor, + ast.While, + ast.If, + ast.With, + ast.AsyncWith, + ast.Try, + ast.Lambda, + ast.IfExp, + ast.ExceptHandler, + ) + if isinstance(node, ast_types_with_nodebody) and isinstance( + node.body, Iterable + ): + for child_nodes in node.body: + top_level_nodes.append(child_nodes) + + exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line) + + # Just return the exact top level line, if present. + if len(exact_nodes) > 0: + which_line_next = 0 + for same_line_node in exact_nodes: + should_run_top_blocks.append(same_line_node) + smart_code += ( + f"{ast.get_source_segment(wholeFileContent, same_line_node)}\n" + ) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": smart_code, + "which_line_next": which_line_next, + } + + # For each of the nodes in the parsed file content, + # add the appropriate source code line(s) to be sent to the REPL, dependent on + # user is trying to send and execute single line/statement or multiple with smart selection. + for top_node in ast.iter_child_nodes(parsed_file_content): + if start_line == top_node.lineno and end_line == top_node.end_lineno: + should_run_top_blocks.append(top_node) + + smart_code += f"{ast.get_source_segment(wholeFileContent, top_node)}\n" + break # If we found exact match, don't waste computation in parsing extra nodes. + elif start_line >= top_node.lineno and end_line <= top_node.end_lineno: + # Case to apply smart selection for multiple line. + # This is the case for when we have to add multiple lines that should be included in the smart send. + # For example: + # 'my_dictionary': { + # 'Audi': 'Germany', + # 'BMW': 'Germany', + # 'Genesis': 'Korea', + # } + # with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary. + + should_run_top_blocks.append(top_node) + + smart_code += str(ast.get_source_segment(wholeFileContent, top_node)) + smart_code += "\n" + + normalized_smart_result = normalize_lines(smart_code) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": normalized_smart_result, + "which_line_next": which_line_next, + } + + +# Look at the last top block added, find lineno for the next upcoming block, +# This will be used in calculating lineOffset to move cursor in VS Code. +def get_next_block_lineno(which_line_next): + last_ran_lineno = int(which_line_next[-1].end_lineno) + next_lineno = int(which_line_next[-1].end_lineno) + + for reverse_node in top_level_nodes: + if reverse_node.lineno > last_ran_lineno: + next_lineno = reverse_node.lineno + break + return next_lineno + + if __name__ == "__main__": # Content is being sent from the extension as a JSON object. # Decode the data from the raw bytes. stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer raw = stdin.read() contents = json.loads(raw.decode("utf-8")) + # Empty highlight means user has not explicitly selected specific text. + empty_Highlight = contents.get("emptyHighlight", False) - normalized = normalize_lines(contents["code"]) + # We also get the activeEditor selection start line and end line from the typescript VS Code side. + # Remember to add 1 to each of the received since vscode starts line counting from 0 . + vscode_start_line = contents["startLine"] + 1 + vscode_end_line = contents["endLine"] + 1 # Send the normalized code back to the extension in a JSON object. - data = json.dumps({"normalized": normalized}) + data = None + which_line_next = 0 + + if empty_Highlight and contents.get("smartSendExperimentEnabled"): + result = traverse_file( + contents["wholeFileContent"], + vscode_start_line, + vscode_end_line, + not empty_Highlight, + ) + normalized = result["normalized_smart_result"] + which_line_next = result["which_line_next"] + data = json.dumps( + {"normalized": normalized, "nextBlockLineno": result["which_line_next"]} + ) + else: + normalized = normalize_lines(contents["code"]) + data = json.dumps({"normalized": normalized}) stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer stdout.write(data.encode("utf-8")) diff --git a/extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py b/extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py index 19565c19675..8e2ed33a1da 100644 --- a/extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py +++ b/extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py @@ -9,7 +9,6 @@ def _check_binaries(dir_path): "win_amd64.pyd", "win32.pyd", "darwin.so", - "i386-linux-gnu.so", "x86_64-linux-gnu.so", ) @@ -18,10 +17,6 @@ def _check_binaries(dir_path): assert len(binaries) == len(expected_endswith) -@pytest.mark.skipif( - sys.version_info[:2] != (3, 7), - reason="DEBUGPY wheels shipped for Python 3.7 only", -) def test_install_debugpy(tmpdir): import install_debugpy diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_env_vars.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_env_vars.py new file mode 100644 index 00000000000..c8a3add5676 --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_env_vars.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +def test_clear_env(monkeypatch): + # Clear all environment variables + monkeypatch.setattr(os, "environ", {}) + + # Now os.environ should be empty + assert not os.environ + + # After the test finishes, the environment variables will be reset to their original state + + +def test_check_env(): + # This test will have access to the original environment variables + assert "PATH" in os.environ + + +def test_clear_env_unsafe(): + # Clear all environment variables + os.environ.clear() + # Now os.environ should be empty + assert not os.environ + + +def test_check_env_unsafe(): + # ("PATH" in os.environ) is False here if it runs after test_clear_env_unsafe. + # Regardless, this test will pass and TEST_PORT and TEST_UUID will still be set correctly + assert "PATH" not in os.environ diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_logging.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_logging.py new file mode 100644 index 00000000000..058ad807571 --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_logging.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +import sys + + +def test_logging2(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) + assert False + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 76d21b3e251..44f3d3d0abc 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -596,3 +596,91 @@ "subtest": None, } } + + +# This is the expected output for the test logging file. +# └── test_logging.py +# └── test_logging2: failure +# └── test_logging: success +test_logging_path = TEST_DATA_PATH / "test_logging.py" + +logging_test_expected_execution_output = { + get_absolute_test_id("test_logging.py::test_logging2", test_logging_path): { + "test": get_absolute_test_id( + "test_logging.py::test_logging2", test_logging_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_logging.py::test_logging", test_logging_path): { + "test": get_absolute_test_id( + "test_logging.py::test_logging", test_logging_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the test safe clear env vars file. +# └── test_env_vars.py +# └── test_clear_env: success +# └── test_check_env: success + +test_safe_clear_env_vars_path = TEST_DATA_PATH / "test_env_vars.py" +safe_clear_env_vars_expected_execution_output = { + get_absolute_test_id( + "test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "test_env_vars.py::test_check_env", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_check_env", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the test unsafe clear env vars file. +# └── test_env_vars.py +# └── test_clear_env_unsafe: success +# └── test_check_env_unsafe: success +unsafe_clear_env_vars_expected_execution_output = { + get_absolute_test_id( + "test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py index b534e950945..2d36da59956 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py @@ -129,6 +129,7 @@ def runner_with_cwd( "pytest", "-p", "vscode_pytest", + "-s", ] + args listener: socket.socket = create_server() _, port = listener.getsockname() diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py index 37a392f66d4..dd32b61fa26 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py @@ -131,6 +131,13 @@ def test_bad_id_error_execution(): @pytest.mark.parametrize( "test_ids, expected_const", [ + ( + [ + "test_env_vars.py::test_clear_env", + "test_env_vars.py::test_check_env", + ], + expected_execution_test_output.safe_clear_env_vars_expected_execution_output, + ), ( [ "skip_tests.py::test_something", @@ -215,23 +222,30 @@ def test_bad_id_error_execution(): ], expected_execution_test_output.doctest_pytest_expected_execution_output, ), + ( + ["test_logging.py::test_logging2", "test_logging.py::test_logging"], + expected_execution_test_output.logging_test_expected_execution_output, + ), ], ) def test_pytest_execution(test_ids, expected_const): """ Test that pytest discovery works as expected where run pytest is always successful but the actual test results are both successes and failures.: - 1. uf_execution_expected_output: unittest tests run on multiple files. - 2. uf_single_file_expected_output: test run on a single file. - 3. uf_single_method_execution_expected_output: test run on a single method in a file. - 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. - 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + 1: skip_tests_execution_expected_output: test run on a file with skipped tests. + 2. error_raised_exception_execution_expected_output: test run on a file that raises an exception. + 3. uf_execution_expected_output: unittest tests run on multiple files. + 4. uf_single_file_expected_output: test run on a single file. + 5. uf_single_method_execution_expected_output: test run on a single method in a file. + 6. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. + 7. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. + 8. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. - 7. double_nested_folder_expected_execution_output: test run on a double nested folder. - 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. - 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. - 10. doctest_pytest_expected_execution_output: test run on doctest file. + 9. double_nested_folder_expected_execution_output: test run on a double nested folder. + 10. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. + 11. single_parametrize_tests_expected_execution_output: test run on single parametrize test. + 12. doctest_pytest_expected_execution_output: test run on doctest file. + 13. logging_test_expected_execution_output: test run on a file with logging. Keyword arguments: diff --git a/extensions/positron-python/pythonFiles/tests/test_dynamic_cursor.py b/extensions/positron-python/pythonFiles/tests/test_dynamic_cursor.py new file mode 100644 index 00000000000..7aea59427aa --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/test_dynamic_cursor.py @@ -0,0 +1,203 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_dictionary_mouse_mover(): + """ + Having the mouse cursor on second line, + 'my_dict = {' + and pressing shift+enter should bring the + mouse cursor to line 6, on and to be able to run + 'print('only send the dictionary')' + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 6 + + +def test_beginning_func(): + """ + Pressing shift+enter on the very first line, + of function definition, such as 'my_func():' + It should properly skip the comment and assert the + next executable line to be executed is line 5 at + 'my_dict = {' + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_func(): + print("line 2") + print("line 3") + # Skip line 4 because it is a comment + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 5 + + +def test_cursor_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + lucid_dream = ["Corgi", "Husky", "Pomsky"] + for dogs in lucid_dream: # initial starting position + print(dogs) + print("I wish I had a dog!") + + print("This should be the next block that should be ran") + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 6 + + +def test_inside_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for food in lucid_dream: + print("We are starting") # initial starting position + print("Next cursor should be here!") + + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 3 + + +def test_skip_sameline_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Audi");print("BMW");print("Mercedes") + print("Next line to be run is here!") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 2 + + +def test_skip_multi_comp_lambda(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + # Shift enter from the very first ( should make + # next executable statement as the lambda expression + assert result["which_line_next"] == 7 + + +def test_move_whole_class(): + """ + Shift+enter on a class definition + should move the cursor after running whole class. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 7 + + +def test_def_to_def(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + # Skip here + def next_func(): + print("Not here but above") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 9 + + +def test_try_catch_move(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Should be here afterwards") + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["which_line_next"] == 6 + + +def test_skip_nested(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + print("Cursor should be here after running line 1") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["which_line_next"] == 8 diff --git a/extensions/positron-python/pythonFiles/tests/test_normalize_selection.py b/extensions/positron-python/pythonFiles/tests/test_normalize_selection.py index 138c5ad2f52..5f4d6d7d4a1 100644 --- a/extensions/positron-python/pythonFiles/tests/test_normalize_selection.py +++ b/extensions/positron-python/pythonFiles/tests/test_normalize_selection.py @@ -1,8 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + +import importlib import textwrap +# __file__ = "/Users/anthonykim/Desktop/vscode-python/pythonFiles/normalizeSelection.py" +# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__)))) import normalizeSelection @@ -215,3 +219,52 @@ def show_something(): ) result = normalizeSelection.normalize_lines(src) assert result == expected + + def test_fstring(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + + print(f'My name is {name}') + """ + ) + + expected = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + print(f'My name is {name}') + """ + ) + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_list_comp(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + expected = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected diff --git a/extensions/positron-python/pythonFiles/tests/test_smart_selection.py b/extensions/positron-python/pythonFiles/tests/test_smart_selection.py new file mode 100644 index 00000000000..b86e6f9dc82 --- /dev/null +++ b/extensions/positron-python/pythonFiles/tests/test_smart_selection.py @@ -0,0 +1,388 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_part_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + expected = textwrap.dedent( + """\ + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 3, 3, False) + assert result["normalized_smart_result"] == expected + + +def test_nested_loop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_smart_shift_enter_multiple_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + import textwrap + import ast + + print("Porsche") + print("Genesis") + + + print("Audi");print("BMW");print("Mercedes") + + print("dont print me") + + """ + ) + # Expected to printing statement line by line, + # for when multiple print statements are ran + # from the same line. + expected = textwrap.dedent( + """\ + print("Audi") + print("BMW") + print("Mercedes") + """ + ) + result = normalizeSelection.traverse_file(src, 8, 8, False) + assert result["normalized_smart_result"] == expected + + +def test_two_layer_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("dont print me") + + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + expected = textwrap.dedent( + """\ + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + result = normalizeSelection.traverse_file(src, 6, 7, False) + + assert result["normalized_smart_result"] == expected + + +def test_run_whole_func(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Decide which dog you will choose") + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + """ + ) + + expected = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["normalized_smart_result"] == expected + + +def test_small_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + + """ + ) + + # Cover the whole for loop block with multiple inner statements + # Make sure to contain all of the print statements included. + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def inner_for_loop_component(): + """ + Pressing shift+enter inside a for loop, + specifically on a viable expression + by itself, such as print(i) + should only return that exact expression + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, False) + expected = textwrap.dedent( + """\ + print(i) + """ + ) + + assert result["normalized_smart_result"] == expected + + +def test_dict_comprehension(): + """ + Having the mouse cursor on the first line, + and pressing shift+enter should return the + whole dictionary comp, respecting user's code style. + """ + + importlib.reload + src = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + expected = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def test_send_whole_generator(): + """ + Pressing shift+enter on the first line, which is the '(' + should be returning the whole generator expression instead of just the '(' + """ + + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + """ + ) + + expected = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def test_multiline_lambda(): + """ + Shift+enter on part of the lambda expression + should return the whole lambda expression, + regardless of whether all the component of + lambda expression is on the same or not. + """ + + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + expected = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_class(): + """ + Shift+enter on a class definition + should send the whole class definition + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + expected = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + """ + ) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_if_statement(): + """ + Shift+enter on an if statement + should send the whole if statement + including statements inside and else. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + print('cursor here afterwards') + """ + ) + expected = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_send_try(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Not running this") + """ + ) + expected = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py index 67e52f43b70..7d7db772a4a 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py @@ -18,7 +18,7 @@ [ ( ["-s", "something", "-p", "other*", "-t", "else"], - ("something", "other*", "else"), + ("something", "other*", "else", 1, None, None), ), ( [ @@ -29,11 +29,35 @@ "--top-level-directory", "baz", ], - ("foo", "bar*", "baz"), + ("foo", "bar*", "baz", 1, None, None), ), ( ["--foo", "something"], - (".", "test*.py", None), + (".", "test*.py", None, 1, None, None), + ), + ( + ["--foo", "something", "-v"], + (".", "test*.py", None, 2, None, None), + ), + ( + ["--foo", "something", "-f"], + (".", "test*.py", None, 1, True, None), + ), + ( + ["--foo", "something", "--verbose", "-f"], + (".", "test*.py", None, 2, True, None), + ), + ( + ["--foo", "something", "-q", "--failfast"], + (".", "test*.py", None, 0, True, None), + ), + ( + ["--foo", "something", "--quiet"], + (".", "test*.py", None, 0, None, None), + ), + ( + ["--foo", "something", "--quiet", "--locals"], + (".", "test*.py", None, 0, None, True), ), ], ) diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py index f7306e37662..7d11c656b57 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py +++ b/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py @@ -22,7 +22,7 @@ def test_no_ids_run() -> None: start_dir: str = os.fspath(TEST_DATA_PATH) testids = [] pattern = "discovery_simple*" - actual = run_tests(start_dir, testids, pattern, None, "fake-uuid") + actual = run_tests(start_dir, testids, pattern, None, "fake-uuid", 1, None) assert actual assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" @@ -41,7 +41,13 @@ def test_single_ids_run() -> None: """ id = "discovery_simple.DiscoverySimple.test_one" actual = run_tests( - os.fspath(TEST_DATA_PATH), [id], "discovery_simple*", None, "fake-uuid" + os.fspath(TEST_DATA_PATH), + [id], + "discovery_simple*", + None, + "fake-uuid", + 1, + None, ) assert actual assert all(item in actual for item in ("cwd", "status")) @@ -65,7 +71,13 @@ def test_subtest_run() -> None: """ id = "test_subtest.NumbersTest.test_even" actual = run_tests( - os.fspath(TEST_DATA_PATH), [id], "test_subtest.py", None, "fake-uuid" + os.fspath(TEST_DATA_PATH), + [id], + "test_subtest.py", + None, + "fake-uuid", + 1, + None, ) subtests_ids = [ "test_subtest.NumbersTest.test_even (i=0)", @@ -162,7 +174,7 @@ def test_multiple_ids_run(test_ids, pattern, cwd, expected_outcome) -> None: All tests should have the outcome of `success`. """ - actual = run_tests(cwd, test_ids, pattern, None, "fake-uuid") + actual = run_tests(cwd, test_ids, pattern, None, "fake-uuid", 1, None) assert actual assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" @@ -186,7 +198,13 @@ def test_failed_tests(): "test_fail_simple.RunFailSimple.test_two_fail", ] actual = run_tests( - os.fspath(TEST_DATA_PATH), test_ids, "test_fail_simple*", None, "fake-uuid" + os.fspath(TEST_DATA_PATH), + test_ids, + "test_fail_simple*", + None, + "fake-uuid", + 1, + None, ) assert actual assert all(item in actual for item in ("cwd", "status")) @@ -202,6 +220,9 @@ def test_failed_tests(): assert "outcome" in id_result assert id_result["outcome"] == "failure" assert "message" and "traceback" in id_result + assert "2 not greater than 3" in str(id_result["message"]) or "1 == 1" in str( + id_result["traceback"] + ) assert True @@ -211,7 +232,13 @@ def test_unknown_id(): """ test_ids = ["unknown_id"] actual = run_tests( - os.fspath(TEST_DATA_PATH), test_ids, "test_fail_simple*", None, "fake-uuid" + os.fspath(TEST_DATA_PATH), + test_ids, + "test_fail_simple*", + None, + "fake-uuid", + 1, + None, ) assert actual assert all(item in actual for item in ("cwd", "status")) @@ -239,6 +266,8 @@ def test_incorrect_path(): "test_fail_simple*", None, "fake-uuid", + 1, + None, ) assert actual assert all(item in actual for item in ("cwd", "status", "error")) diff --git a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py index 7e07e45d120..274fb5e5e66 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/discovery.py @@ -19,7 +19,7 @@ # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -DEFAULT_PORT = "45454" +DEFAULT_PORT = 45454 class PayloadDict(TypedDict): @@ -37,7 +37,10 @@ class EOTPayloadDict(TypedDict): def discover_tests( - start_dir: str, pattern: str, top_level_dir: Optional[str], uuid: Optional[str] + start_dir: str, + pattern: str, + top_level_dir: Optional[str], + uuid: Optional[str], ) -> PayloadDict: """Returns a dictionary containing details of the discovered tests. @@ -119,14 +122,27 @@ def post_response( argv = sys.argv[1:] index = argv.index("--udiscovery") - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + ( + start_dir, + pattern, + top_level_dir, + _verbosity, + _failfast, + _locals, + ) = parse_unittest_args(argv[index + 1 :]) - # Perform test discovery. testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) testUuid = os.environ.get("TEST_UUID") - # Post this discovery payload. + if testPort is DEFAULT_PORT: + print( + "Error[vscode-unittest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) if testUuid is not None: + # Perform test discovery. payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + # Post this discovery payload. post_response(payload, testPort, testUuid) # Post EOT token. eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} diff --git a/extensions/positron-python/pythonFiles/unittestadapter/execution.py b/extensions/positron-python/pythonFiles/unittestadapter/execution.py index 0684ada8e44..769d70afc0d 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/execution.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/execution.py @@ -21,14 +21,13 @@ from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args -DEFAULT_PORT = "45454" - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] testPort = 0 testUuid = 0 START_DIR = "" +DEFAULT_PORT = 45454 class TestOutcomeEnum(str, enum.Enum): @@ -104,13 +103,18 @@ def formatResult( subtest: Union[unittest.TestCase, None] = None, ): tb = None - if error and error[2] is not None: - # Format traceback + + message = "" + # error is a tuple of the form returned by sys.exc_info(): (type, value, traceback). + if error is not None: + try: + message = f"{error[0]} {error[1]}" + except Exception: + message = "Error occurred, unknown type or value" formatted = traceback.format_exception(*error) + tb = "".join(formatted) # Remove the 'Traceback (most recent call last)' formatted = formatted[1:] - tb = "".join(formatted) - if subtest: test_id = subtest.id() else: @@ -119,7 +123,7 @@ def formatResult( result = { "test": test.id(), "outcome": outcome, - "message": str(error), + "message": message, "traceback": tb, "subtest": subtest.id() if subtest else None, } @@ -163,6 +167,9 @@ def run_tests( pattern: str, top_level_dir: Optional[str], uuid: Optional[str], + verbosity: int, + failfast: Optional[bool], + locals: Optional[bool] = None, ) -> PayloadDict: cwd = os.path.abspath(start_dir) status = TestExecutionStatus.error @@ -186,8 +193,18 @@ def run_tests( } suite = loader.discover(start_dir, pattern, top_level_dir) # noqa: F841 - # Run tests. - runner = unittest.TextTestRunner(resultclass=UnittestTestResult) + if failfast is None: + failfast = False + if locals is None: + locals = False + if verbosity is None: + verbosity = 1 + runner = unittest.TextTestRunner( + resultclass=UnittestTestResult, + tb_locals=locals, + failfast=failfast, + verbosity=verbosity, + ) # lets try to tailer our own suite so we can figure out running only the ones we want loader = unittest.TestLoader() tailor: unittest.TestSuite = loader.loadTestsFromNames(test_ids) @@ -258,13 +275,21 @@ def post_response( argv = sys.argv[1:] index = argv.index("--udiscovery") - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + ( + start_dir, + pattern, + top_level_dir, + verbosity, + failfast, + locals, + ) = parse_unittest_args(argv[index + 1 :]) run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 ) - + if run_test_ids_port_int == 0: + print("Error[vscode-unittest]: RUN_TEST_IDS_PORT env var is not set.") # get data from socket test_ids_from_buffer = [] try: @@ -288,8 +313,6 @@ def post_response( ) # Clear the buffer as complete JSON object is received buffer = b"" - - # Process the JSON data break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received @@ -300,10 +323,30 @@ def post_response( testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) testUuid = os.environ.get("TEST_UUID") + if testPort is DEFAULT_PORT: + print( + "Error[vscode-unittest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) + if testUuid is None: + print( + "Error[vscode-unittest]: TEST_UUID is not set.", + " TEST_PORT = ", + testPort, + ) + testUuid = "unknown" if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid + start_dir, + test_ids_from_buffer, + pattern, + top_level_dir, + testUuid, + verbosity, + failfast, + locals, ) else: cwd = os.path.abspath(start_dir) diff --git a/extensions/positron-python/pythonFiles/unittestadapter/utils.py b/extensions/positron-python/pythonFiles/unittestadapter/utils.py index 64f08217f38..2c5ebf09abc 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/utils.py +++ b/extensions/positron-python/pythonFiles/unittestadapter/utils.py @@ -217,7 +217,9 @@ def build_test_tree( return root, error -def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: +def parse_unittest_args( + args: List[str], +) -> Tuple[str, str, Union[str, None], int, Union[bool, None], Union[bool, None]]: """Parse command-line arguments that should be forwarded to unittest to perform discovery. Valid unittest arguments are: -v, -s, -p, -t and their long-form counterparts, @@ -234,11 +236,24 @@ def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: arg_parser.add_argument("--start-directory", "-s", default=".") arg_parser.add_argument("--pattern", "-p", default="test*.py") arg_parser.add_argument("--top-level-directory", "-t", default=None) + arg_parser.add_argument("--failfast", "-f", action="store_true", default=None) + arg_parser.add_argument("--verbose", "-v", action="store_true", default=None) + arg_parser.add_argument("-q", "--quiet", action="store_true", default=None) + arg_parser.add_argument("--locals", action="store_true", default=None) parsed_args, _ = arg_parser.parse_known_args(args) + verbosity: int = 1 + if parsed_args.quiet: + verbosity = 0 + elif parsed_args.verbose: + verbosity = 2 + return ( parsed_args.start_directory, parsed_args.pattern, parsed_args.top_level_directory, + verbosity, + parsed_args.failfast, + parsed_args.locals, ) diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py index 2fab4d77c2f..6f5687357f1 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/__init__.py @@ -6,7 +6,6 @@ import os import pathlib import sys -import time import traceback import pytest @@ -20,6 +19,8 @@ from testing_tools import socket_manager from typing_extensions import Literal, TypedDict +DEFAULT_PORT = 45454 + class TestData(TypedDict): """A general class that all test objects inherit from.""" @@ -54,9 +55,21 @@ def __init__(self, message): IS_DISCOVERY = False map_id_to_path = dict() collected_tests_so_far = list() +TEST_PORT = os.getenv("TEST_PORT") +TEST_UUID = os.getenv("TEST_UUID") def pytest_load_initial_conftests(early_config, parser, args): + global TEST_PORT + global TEST_UUID + TEST_PORT = os.getenv("TEST_PORT") + TEST_UUID = os.getenv("TEST_UUID") + error_string = ( + "PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being" + " changed or removed as they are required for successful test discovery and execution." + f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n" + ) + print(error_string, file=sys.stderr) if "--collect-only" in args: global IS_DISCOVERY IS_DISCOVERY = True @@ -181,6 +194,7 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): tests: Dict[str, TestOutcome] +@pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_report_teststatus(report, config): """ A pytest hook that is called when a test is called. It is called 3 times per test, @@ -221,6 +235,7 @@ def pytest_report_teststatus(report, config): "success", collected_test if collected_test else None, ) + yield ERROR_MESSAGE_CONST = { @@ -231,6 +246,7 @@ def pytest_report_teststatus(report, config): } +@pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_runtest_protocol(item, nextitem): map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) @@ -253,6 +269,7 @@ def pytest_runtest_protocol(item, nextitem): "success", collected_test if collected_test else None, ) + yield def check_skipped_wrapper(item): @@ -300,12 +317,12 @@ def pytest_sessionfinish(session, exitstatus): session -- the pytest session object. exitstatus -- the status code of the session. - 0: All tests passed successfully. - 1: One or more tests failed. - 2: Pytest was unable to start or run any tests due to issues with test discovery or test collection. - 3: Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution. - 4: Pytest encountered an internal error or exception during test execution. - 5: Pytest was unable to find any tests to run. + Exit code 0: All tests were collected and passed successfully + Exit code 1: Tests were collected and run but some of the tests failed + Exit code 2: Test execution was interrupted by the user + Exit code 3: Internal error happened while executing tests + Exit code 4: pytest command line usage error + Exit code 5: No tests were collected """ cwd = pathlib.Path.cwd() if IS_DISCOVERY: @@ -616,7 +633,13 @@ class EOTPayloadDict(TypedDict): def get_node_path(node: Any) -> pathlib.Path: """A function that returns the path of a node given the switch to pathlib.Path.""" - return getattr(node, "path", pathlib.Path(node.fspath)) + path = getattr(node, "path", None) or pathlib.Path(node.fspath) + + if not path: + raise VSCodePytestError( + f"Unable to find path for node: {node}, node.path: {node.path}, node.fspath: {node.fspath}" + ) + return path __socket = None @@ -683,9 +706,19 @@ def send_post_request( payload -- the payload data to be sent. cls_encoder -- a custom encoder if needed. """ - testPort = os.getenv("TEST_PORT", 45454) - testUuid = os.getenv("TEST_UUID") - addr = ("localhost", int(testPort)) + global TEST_PORT + global TEST_UUID + if TEST_UUID is None or TEST_PORT is None: + # if TEST_UUID or TEST_PORT is None, print an error and fail as these are both critical errors + error_msg = ( + "PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being" + " changed or removed as they are required for successful pytest discovery and execution." + f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodePytestError(error_msg) + + addr = ("localhost", int(TEST_PORT)) global __socket if __socket is None: @@ -693,34 +726,34 @@ def send_post_request( __socket = socket_manager.SocketManager(addr) __socket.connect() except Exception as error: - print(f"Plugin error connection error[vscode-pytest]: {error}") + error_msg = f"Error attempting to connect to extension communication socket[vscode-pytest]: {error}" + print(error_msg, file=sys.stderr) + print( + "If you are on a Windows machine, this error may be occurring if any of your tests clear environment variables" + " as they are required to communicate with the extension. Please reference https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-environment-variables" + "for the correct way to clear environment variables during testing.\n", + file=sys.stderr, + ) __socket = None + raise VSCodePytestError(error_msg) data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {testUuid} +Request-uuid: {TEST_UUID} {data}""" - max_retries = 3 - retries = 0 - while retries < max_retries: - try: - if __socket is not None and __socket.socket is not None: - __socket.socket.sendall(request.encode("utf-8")) - # print("Post request sent successfully!") - # print("data sent", payload, "end of data") - break # Exit the loop if the send was successful - else: - print("Plugin error connection error[vscode-pytest]") - print(f"[vscode-pytest] data: {request}") - except Exception as error: - print(f"Plugin error connection error[vscode-pytest]: {error}") - print(f"[vscode-pytest] data: {request}") - retries += 1 # Increment retry counter - if retries < max_retries: - print(f"Retrying ({retries}/{max_retries}) in 2 seconds...") - time.sleep(2) # Wait for a short duration before retrying - else: - print("Maximum retry attempts reached. Cannot send post request.") + try: + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + else: + print( + f"Plugin error connection error[vscode-pytest], socket is None \n[vscode-pytest] data: \n{request} \n", + file=sys.stderr, + ) + except Exception as error: + print( + f"Plugin error, exception thrown while attempting to send data[vscode-pytest]: {error} \n[vscode-pytest] data: \n{request}\n", + file=sys.stderr, + ) diff --git a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py index 0fca8208a40..e60ee91f096 100644 --- a/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/extensions/positron-python/pythonFiles/vscode_pytest/run_pytest_script.py @@ -28,6 +28,8 @@ run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 ) + if run_test_ids_port_int == 0: + print("Error[vscode-pytest]: RUN_TEST_IDS_PORT env var is not set.") test_ids_from_buffer = [] try: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -51,8 +53,6 @@ ) # Clear the buffer as complete JSON object is received buffer = b"" - - # Process the JSON data print("Received JSON data in run script") break except json.JSONDecodeError: diff --git a/extensions/positron-python/requirements.in b/extensions/positron-python/requirements.in index 6701a1ef1b7..22b599619ad 100644 --- a/extensions/positron-python/requirements.in +++ b/extensions/positron-python/requirements.in @@ -4,7 +4,7 @@ # 2) pip-compile --generate-hashes requirements.in # Unittest test adapter -typing-extensions==4.7.1 +typing-extensions==4.8.0 # Fallback env creator for debian microvenv diff --git a/extensions/positron-python/requirements.txt b/extensions/positron-python/requirements.txt index 205b9fc4804..c24b0b48e39 100644 --- a/extensions/positron-python/requirements.txt +++ b/extensions/positron-python/requirements.txt @@ -8,9 +8,9 @@ importlib-metadata==6.7.0 \ --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 # via -r requirements.in -microvenv==2023.2.0 \ - --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ - --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 +microvenv==2023.5 \ + --hash=sha256:128c0c8ab46e3bbd7b4c902c8a5d6333b694f9ebf871f123b473425cb6fbe19f \ + --hash=sha256:270977691d207d70308c4239221d2ffbbfd595fa1819d09680c75e8808b21254 # via -r requirements.in packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ @@ -20,9 +20,9 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.7.1 \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ diff --git a/extensions/positron-python/resources/report_issue_user_settings.json b/extensions/positron-python/resources/report_issue_user_settings.json index 778434c5cf0..eea4ca007da 100644 --- a/extensions/positron-python/resources/report_issue_user_settings.json +++ b/extensions/positron-python/resources/report_issue_user_settings.json @@ -69,19 +69,6 @@ "memory": true, "symbolsHierarchyDepthLimit": false }, - "sortImports": { - "args": "placeholder", - "path": "placeholder" - }, - "formatting": { - "autopep8Args": "placeholder", - "autopep8Path": "placeholder", - "provider": true, - "blackArgs": "placeholder", - "blackPath": "placeholder", - "yapfArgs": "placeholder", - "yapfPath": "placeholder" - }, "testing": { "cwd": "placeholder", "debugPort": true, diff --git a/extensions/positron-python/scripts/onCreateCommand.sh b/extensions/positron-python/scripts/onCreateCommand.sh index 6303d21ef48..05ffe64a8c0 100644 --- a/extensions/positron-python/scripts/onCreateCommand.sh +++ b/extensions/positron-python/scripts/onCreateCommand.sh @@ -26,7 +26,8 @@ pyenv exec python3.8 -m venv .venv source /workspaces/vscode-python/.venv/bin/activate # Install required Python libraries. -npx gulp installPythonLibs +/workspaces/vscode-python/.venv/bin/python -m pip install nox +nox --session install_python_libs /workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt /workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt diff --git a/extensions/positron-python/src/client/api.ts b/extensions/positron-python/src/client/api.ts index 23b2553c93d..81a5f676cc2 100644 --- a/extensions/positron-python/src/client/api.ts +++ b/extensions/positron-python/src/client/api.ts @@ -21,6 +21,7 @@ import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; import { ApiForPylance } from './pylanceApi'; import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; export function buildApi( ready: Promise, @@ -31,7 +32,14 @@ export function buildApi( const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: PythonExtension & { @@ -41,6 +49,12 @@ export function buildApi( jupyter: { registerHooks(): void; }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an @@ -92,6 +106,9 @@ export function buildApi( jupyter: { registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, debug: { async getRemoteLauncherCommand( host: string, diff --git a/extensions/positron-python/src/client/common/application/applicationShell.ts b/extensions/positron-python/src/client/common/application/applicationShell.ts index 45466247201..aadf8018690 100644 --- a/extensions/positron-python/src/client/common/application/applicationShell.ts +++ b/extensions/positron-python/src/client/common/application/applicationShell.ts @@ -10,6 +10,7 @@ import { DocumentSelector, env, Event, + EventEmitter, InputBox, InputBoxOptions, languages, @@ -37,7 +38,8 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { IApplicationShell } from './types'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -172,4 +174,12 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } + public get onDidWriteTerminalData(): Event { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } } diff --git a/extensions/positron-python/src/client/common/application/commands.ts b/extensions/positron-python/src/client/common/application/commands.ts index b408aae5218..8813768d35a 100644 --- a/extensions/positron-python/src/client/common/application/commands.ts +++ b/extensions/positron-python/src/client/common/application/commands.ts @@ -93,7 +93,6 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }]; [Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]]; [Commands.TriggerEnvironmentSelection]: [undefined | Uri]; - [Commands.Sort_Imports]: [undefined, Uri]; [Commands.Exec_In_Terminal]: [undefined, Uri]; [Commands.Exec_In_Terminal_Icon]: [undefined, Uri]; [Commands.Debug_In_Terminal]: [Uri]; @@ -103,4 +102,12 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; [Commands.LaunchTensorBoard]: [TensorBoardEntrypoint, TensorBoardEntrypointTrigger]; ['workbench.view.testing.focus']: []; + ['cursorMove']: [ + { + to: string; + by: string; + value: number; + }, + ]; + ['cursorEnd']: []; } diff --git a/extensions/positron-python/src/client/common/application/progressService.ts b/extensions/positron-python/src/client/common/application/progressService.ts new file mode 100644 index 00000000000..fb19cad1136 --- /dev/null +++ b/extensions/positron-python/src/client/common/application/progressService.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ProgressOptions } from 'vscode'; +import { Deferred, createDeferred } from '../utils/async'; +import { IApplicationShell } from './types'; + +export class ProgressService { + private deferred: Deferred | undefined; + + constructor(private readonly shell: IApplicationShell) {} + + public showProgress(options: ProgressOptions): void { + if (!this.deferred) { + this.createProgress(options); + } + } + + public hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + + private createProgress(options: ProgressOptions) { + this.shell.withProgress(options, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/extensions/positron-python/src/client/common/application/types.ts b/extensions/positron-python/src/client/common/application/types.ts index fa2ced6c45d..863f5e4651b 100644 --- a/extensions/positron-python/src/client/common/application/types.ts +++ b/extensions/positron-python/src/client/common/application/types.ts @@ -67,6 +67,17 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { /** @@ -75,6 +86,13 @@ export interface IApplicationShell { */ readonly onDidChangeWindowState: Event; + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** diff --git a/extensions/positron-python/src/client/common/configSettings.ts b/extensions/positron-python/src/client/common/configSettings.ts index 09b11b2b372..5a163cc06f1 100644 --- a/extensions/positron-python/src/client/common/configSettings.ts +++ b/extensions/positron-python/src/client/common/configSettings.ts @@ -6,7 +6,6 @@ import * as fs from 'fs'; import { ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, Event, EventEmitter, @@ -27,12 +26,9 @@ import { IAutoCompleteSettings, IDefaultLanguageServer, IExperiments, - IFormattingSettings, IInterpreterPathService, IInterpreterSettings, - ILintingSettings, IPythonSettings, - ISortImportSettings, ITensorBoardSettings, ITerminalSettings, Resource, @@ -108,10 +104,6 @@ export class PythonSettings implements IPythonSettings { public devOptions: string[] = []; - public linting!: ILintingSettings; - - public formatting!: IFormattingSettings; - public autoComplete!: IAutoCompleteSettings; public tensorBoard: ITensorBoardSettings | undefined; @@ -120,8 +112,6 @@ export class PythonSettings implements IPythonSettings { public terminal!: ITerminalSettings; - public sortImports!: ISortImportSettings; - public globalModuleInstallation = false; public experiments!: IExperiments; @@ -331,130 +321,8 @@ export class PythonSettings implements IPythonSettings { this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; - if (this.linting) { - Object.assign(this.linting, lintingSettings); - } else { - this.linting = lintingSettings; - } - this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; - if (this.sortImports) { - Object.assign(this.sortImports, sortImportSettings); - } else { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; - // Support for travis. - this.linting = this.linting - ? this.linting - : { - enabled: false, - cwd: undefined, - ignorePatterns: [], - flake8Args: [], - flake8Enabled: false, - flake8Path: 'flake8', - lintOnSave: false, - maxNumberOfProblems: 100, - mypyArgs: [], - mypyEnabled: false, - mypyPath: 'mypy', - banditArgs: [], - banditEnabled: false, - banditPath: 'bandit', - pycodestyleArgs: [], - pycodestyleEnabled: false, - pycodestylePath: 'pycodestyle', - pylamaArgs: [], - pylamaEnabled: false, - pylamaPath: 'pylama', - prospectorArgs: [], - prospectorEnabled: false, - prospectorPath: 'prospector', - pydocstyleArgs: [], - pydocstyleEnabled: false, - pydocstylePath: 'pydocstyle', - pylintArgs: [], - pylintEnabled: false, - pylintPath: 'pylint', - pylintCategorySeverity: { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }, - pycodestyleCategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - }, - flake8CategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code - // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as - // unused imports, variables, etc. - F: DiagnosticSeverity.Warning, - }, - mypyCategorySeverity: { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint, - }, - }; - this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); - this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); - this.linting.pycodestylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pycodestylePath), - workspaceRoot, - ); - this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); - this.linting.prospectorPath = getAbsolutePath( - systemVariables.resolveAny(this.linting.prospectorPath), - workspaceRoot, - ); - this.linting.pydocstylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pydocstylePath), - workspaceRoot, - ); - this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); - - if (this.linting.cwd) { - this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot); - } - - const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; - if (this.formatting) { - Object.assign(this.formatting, formattingSettings); - } else { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting - ? this.formatting - : { - autopep8Args: [], - autopep8Path: 'autopep8', - provider: 'autopep8', - blackArgs: [], - blackPath: 'black', - yapfArgs: [], - yapfPath: 'yapf', - }; - this.formatting.autopep8Path = getAbsolutePath( - systemVariables.resolveAny(this.formatting.autopep8Path), - workspaceRoot, - ); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - this.formatting.blackPath = getAbsolutePath( - systemVariables.resolveAny(this.formatting.blackPath), - workspaceRoot, - ); - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; if (this.testing) { Object.assign(this.testing, testSettings); diff --git a/extensions/positron-python/src/client/common/constants.ts b/extensions/positron-python/src/client/common/constants.ts index 07a28301128..1f493dc436c 100644 --- a/extensions/positron-python/src/client/common/constants.ts +++ b/extensions/positron-python/src/client/common/constants.ts @@ -23,6 +23,7 @@ export const PYTHON_NOTEBOOKS = [ export const PVSC_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; +export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; export type Channel = 'stable' | 'insiders'; @@ -61,7 +62,6 @@ export namespace Commands { export const ReportIssue = 'python.reportIssue'; export const Set_Interpreter = 'python.setInterpreter'; 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'; diff --git a/extensions/positron-python/src/client/common/editor.ts b/extensions/positron-python/src/client/common/editor.ts deleted file mode 100644 index f08d73194d4..00000000000 --- a/extensions/positron-python/src/client/common/editor.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { Diff, diff_match_patch } from 'diff-match-patch'; -import { injectable } from 'inversify'; -import * as md5 from 'md5'; -import { EOL } from 'os'; -import * as path from 'path'; -import { Position, Range, TextDocument, TextEdit, Uri, WorkspaceEdit } from 'vscode'; -import { IFileSystem } from '../common/platform/types'; -import { traceError } from '../logging'; -import { WrappedError } from './errors/errorUtils'; -import { IEditorUtils } from './types'; -import { isNotebookCell } from './utils/misc'; - -// Code borrowed from goFormat.ts (Go Extension for VS Code) -enum EditAction { - Delete, - Insert, - Replace, -} - -const NEW_LINE_LENGTH = EOL.length; - -class Patch { - public diffs!: Diff[]; - public start1!: number; - public start2!: number; - public length1!: number; - public length2!: number; -} - -class Edit { - public action: EditAction; - public start: Position; - public end!: Position; - public text: string; - - constructor(action: number, start: Position) { - this.action = action; - this.start = start; - this.text = ''; - } - - public apply(): TextEdit { - switch (this.action) { - case EditAction.Insert: - return TextEdit.insert(this.start, this.text); - case EditAction.Delete: - return TextEdit.delete(new Range(this.start, this.end)); - case EditAction.Replace: - return TextEdit.replace(new Range(this.start, this.end), this.text); - default: - return new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); - } - } -} - -export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] { - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return []; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - const textEdits: TextEdit[] = []; - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - getTextEditsInternal(before, p.diffs, p.start1).forEach((edit) => textEdits.push(edit.apply())); - }); - - return textEdits; -} -export function getWorkspaceEditsFromPatch( - filePatches: string[], - workspaceRoot: string | undefined, - fs: IFileSystem, -): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - filePatches.forEach((patch) => { - const indexOfAtAt = patch.indexOf('@@'); - if (indexOfAtAt === -1) { - return; - } - const fileNameLines = patch - .substring(0, indexOfAtAt) - .split(/\r?\n/g) - .map((line) => line.trim()) - .filter((line) => line.length > 0 && line.toLowerCase().endsWith('.py') && line.indexOf(' a') > 0); - - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(indexOfAtAt); - } - if (patch.length === 0) { - return; - } - // We can't find the find name - if (fileNameLines.length === 0) { - return; - } - - let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); - fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; - if (!fs.fileExistsSync(fileName)) { - return; - } - - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - const fileSource = fs.readFileSync(fileName); - const fileUri = Uri.file(fileName); - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - - getTextEditsInternal(fileSource, p.diffs, p.start1).forEach((edit) => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(fileUri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(fileUri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(fileUri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - }); - - return workspaceEdit; -} - -function getTextEditsInternal(before: string, diffs: [number, string][], startLine: number = 0): Edit[] { - let line = startLine; - let character = 0; - const beforeLines = before.split(/\r?\n/g); - if (line > 0) { - beforeLines.filter((_l, i) => i < line).forEach((l) => (character += l.length + NEW_LINE_LENGTH)); - } - const edits: Edit[] = []; - let edit: Edit | null = null; - let end: Position; - - for (let i = 0; i < diffs.length; i += 1) { - let start = new Position(line, character); - // Compute the line/character after the diff is applied. - - for (let curr = 0; curr < diffs[i][1].length; curr += 1) { - if (diffs[i][1][curr] !== '\n') { - character += 1; - } else { - character = 0; - line += 1; - } - } - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - switch (diffs[i][0]) { - case dmp.DIFF_DELETE: - if ( - beforeLines[line - 1].length === 0 && - beforeLines[start.line - 1] && - beforeLines[start.line - 1].length === 0 - ) { - // We're asked to delete an empty line which only contains `/\r?\n/g`. The last line is also empty. - // Delete the `\n` from the last line instead of deleting `\n` from the current line - // This change ensures that the last line in the file, which won't contain `\n` is deleted - start = new Position(start.line - 1, 0); - end = new Position(line - 1, 0); - } else { - end = new Position(line, character); - } - if (edit === null) { - edit = new Edit(EditAction.Delete, start); - } else if (edit.action !== EditAction.Delete) { - throw new Error('cannot format due to an internal error.'); - } - edit.end = end; - break; - - case dmp.DIFF_INSERT: - if (edit === null) { - edit = new Edit(EditAction.Insert, start); - } else if (edit.action === EditAction.Delete) { - edit.action = EditAction.Replace; - } - // insert and replace edits are all relative to the original state - // of the document, so inserts should reset the current line/character - // position to the start. - line = start.line; - character = start.character; - edit.text += diffs[i][1]; - break; - - case dmp.DIFF_EQUAL: - if (edit !== null) { - edits.push(edit); - edit = null; - } - break; - } - } - - if (edit !== null) { - edits.push(edit); - } - - return edits; -} - -export async function getTempFileWithDocumentContents(document: TextDocument, fs: IFileSystem): Promise { - // Don't create file in temp folder since external utilities - // look into configuration files in the workspace and are not - // to find custom rules if file is saved in a random disk location. - // This means temp file has to be created in the same folder - // as the original one and then removed. - // Use a .tmp file extension (instead of the original extension) - // because the language server is watching the file system for Python - // file add/delete/change and we don't want this temp file to trigger it. - - let fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath + document.uri.fragment)}.tmp`; - try { - // When dealing with untitled notebooks, there's no original physical file, hence create a temp file. - if (isNotebookCell(document.uri) && !(await fs.fileExists(document.uri.fsPath))) { - fileName = ( - await fs.createTemporaryFile(`${path.basename(document.uri.fsPath)}-${document.uri.fragment}.tmp`) - ).filePath; - } - await fs.writeFile(fileName, document.getText()); - } catch (ex) { - traceError('Failed to create a temporary file', ex); - const exception = ex as Error; - throw new WrappedError(`Failed to create a temporary file, ${exception.message}`, exception); - } - return fileName; -} - -/** - * Parse a textual representation of patches and return a list of Patch objects. - * @param {string} textline Text representation of patches. - * @return {!Array.} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -function patch_fromText(textline: string): Patch[] { - const patches: Patch[] = []; - if (!textline) { - return patches; - } - // Start Modification by Don Jayamanne 24/06/2016 Support for CRLF - const text = textline.split(/[\r\n]/); - // End Modification - let textPointer = 0; - const patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; - while (textPointer < text.length) { - const m = text[textPointer].match(patchHeader); - if (!m) { - throw new Error(`Invalid patch string: ${text[textPointer]}`); - } - - const patch = new (diff_match_patch).patch_obj(); - patches.push(patch); - patch.start1 = parseInt(m[1], 10); - if (m[2] === '') { - patch.start1 -= 1; - patch.length1 = 1; - } else if (m[2] === '0') { - patch.length1 = 0; - } else { - patch.start1 -= 1; - patch.length1 = parseInt(m[2], 10); - } - - patch.start2 = parseInt(m[3], 10); - if (m[4] === '') { - patch.start2 -= 1; - patch.length2 = 1; - } else if (m[4] === '0') { - patch.length2 = 0; - } else { - patch.start2 -= 1; - patch.length2 = parseInt(m[4], 10); - } - textPointer += 1; - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - while (textPointer < text.length) { - const sign = text[textPointer].charAt(0); - let line: string; - try { - //var line = decodeURI(text[textPointer].substring(1)); - // For some reason the patch generated by python files don't encode any characters - // And this patch module (code from Google) is expecting the text to be encoded!! - // Temporary solution, disable decoding - // Issue #188 - line = text[textPointer].substring(1); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in patch_fromText'); - } - if (sign === '-') { - // Deletion. - patch.diffs.push([dmp.DIFF_DELETE, line]); - } else if (sign === '+') { - // Insertion. - patch.diffs.push([dmp.DIFF_INSERT, line]); - } else if (sign === ' ') { - // Minor equality. - patch.diffs.push([dmp.DIFF_EQUAL, line]); - } else if (sign === '@') { - // Start of next patch. - break; - } else if (sign === '') { - // Blank line? Whatever. - } else { - throw new Error(`Invalid patch mode '${sign}' in: ${line}`); - } - textPointer += 1; - } - } - return patches; -} - -@injectable() -export class EditorUtils implements IEditorUtils { - public getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return workspaceEdit; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - getTextEditsInternal(originalContents, p.diffs, p.start1).forEach((edit) => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(uri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(uri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(uri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - - return workspaceEdit; - } -} diff --git a/extensions/positron-python/src/client/common/experiments/groups.ts b/extensions/positron-python/src/client/common/experiments/groups.ts index 1ee06469095..8f8ecc631ca 100644 --- a/extensions/positron-python/src/client/common/experiments/groups.ts +++ b/extensions/positron-python/src/client/common/experiments/groups.ts @@ -11,10 +11,16 @@ export enum TerminalEnvVarActivation { experiment = 'pythonTerminalEnvVarActivation', } -export enum ShowFormatterExtensionPrompt { - experiment = 'pythonPromptNewFormatterExt', -} // Experiment to enable the new testing rewrite. export enum EnableTestAdapterRewrite { experiment = 'pythonTestAdapter', } +// Experiment to enable smart shift+enter, advance cursor. +export enum EnableREPLSmartSend { + experiment = 'pythonREPLSmartSend', +} + +// Experiment to recommend installing the tensorboard extension. +export enum RecommendTensobardExtension { + experiment = 'pythonRecommendTensorboardExt', +} diff --git a/extensions/positron-python/src/client/common/experiments/helpers.ts b/extensions/positron-python/src/client/common/experiments/helpers.ts index 50a471f2381..079342560db 100644 --- a/extensions/positron-python/src/client/common/experiments/helpers.ts +++ b/extensions/positron-python/src/client/common/experiments/helpers.ts @@ -7,10 +7,12 @@ import { env, workspace } from 'vscode'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; import { isTestExecution } from '../constants'; +import { traceInfo } from '../../logging'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { - if (!isTestExecution() && workspace.workspaceFile && env.remoteName) { + if (!isTestExecution() && env.remoteName && workspace.workspaceFolders && workspace.workspaceFolders.length > 1) { // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + traceInfo('Not enabling terminal env var experiment in multiroot remote workspaces'); return false; } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { diff --git a/extensions/positron-python/src/client/common/experiments/service.ts b/extensions/positron-python/src/client/common/experiments/service.ts index 270f9151280..3d85b99a26f 100644 --- a/extensions/positron-python/src/client/common/experiments/service.ts +++ b/extensions/positron-python/src/client/common/experiments/service.ts @@ -257,8 +257,10 @@ function sendOptInOptOutTelemetry(optedIn: string[], optedOut: string[], package const sanitizedOptedIn = optedIn.filter((exp) => optedInEnumValues.includes(exp)); const sanitizedOptedOut = optedOut.filter((exp) => optedOutEnumValues.includes(exp)); + JSON.stringify(sanitizedOptedIn.sort()); + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS, undefined, { - optedInto: sanitizedOptedIn, - optedOutFrom: sanitizedOptedOut, + optedInto: JSON.stringify(sanitizedOptedIn.sort()), + optedOutFrom: JSON.stringify(sanitizedOptedOut.sort()), }); } diff --git a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts index 0268b26ae7e..062d9366d32 100644 --- a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts @@ -31,7 +31,7 @@ export abstract class ModuleInstaller implements IModuleInstaller { public abstract get type(): ModuleInstallerType; - constructor(protected serviceContainer: IServiceContainer) { } + constructor(protected serviceContainer: IServiceContainer) {} public async installModule( productOrModuleName: Product | string, @@ -243,32 +243,10 @@ export abstract class ModuleInstaller implements IModuleInstaller { export function translateProductToModule(product: Product): string { switch (product) { - case Product.mypy: - return 'mypy'; - case Product.pylama: - return 'pylama'; - case Product.prospector: - return 'prospector'; - case Product.pylint: - return 'pylint'; case Product.pytest: return 'pytest'; - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; - case Product.pycodestyle: - return 'pycodestyle'; - case Product.pydocstyle: - return 'pydocstyle'; - case Product.yapf: - return 'yapf'; - case Product.flake8: - return 'flake8'; case Product.unittest: return 'unittest'; - case Product.bandit: - return 'bandit'; case Product.ipykernel: return 'ipykernel'; case Product.tensorboard: diff --git a/extensions/positron-python/src/client/common/installer/productNames.ts b/extensions/positron-python/src/client/common/installer/productNames.ts index f91a815f7c3..4725b01aabb 100644 --- a/extensions/positron-python/src/client/common/installer/productNames.ts +++ b/extensions/positron-python/src/client/common/installer/productNames.ts @@ -4,18 +4,7 @@ import { Product } from '../types'; export const ProductNames = new Map(); -ProductNames.set(Product.autopep8, 'autopep8'); -ProductNames.set(Product.bandit, 'bandit'); -ProductNames.set(Product.black, 'black'); -ProductNames.set(Product.flake8, 'flake8'); -ProductNames.set(Product.mypy, 'mypy'); -ProductNames.set(Product.pycodestyle, 'pycodestyle'); -ProductNames.set(Product.pylama, 'pylama'); -ProductNames.set(Product.prospector, 'prospector'); -ProductNames.set(Product.pydocstyle, 'pydocstyle'); -ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); -ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); diff --git a/extensions/positron-python/src/client/common/installer/productPath.ts b/extensions/positron-python/src/client/common/installer/productPath.ts index 5c36a6bbd3b..b06e4b7a48a 100644 --- a/extensions/positron-python/src/client/common/installer/productPath.ts +++ b/extensions/positron-python/src/client/common/installer/productPath.ts @@ -6,9 +6,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IFormatterHelper } from '../../formatters/types'; import { IServiceContainer } from '../../ioc/types'; -import { ILinterManager } from '../../linters/types'; import { ITestingService } from '../../testing/types'; import { IConfigurationService, IInstaller, Product } from '../types'; import { IProductPathService } from './types'; @@ -37,30 +35,6 @@ export abstract class BaseProductPathsService implements IProductPathService { } } -@injectable() -export class FormatterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const formatHelper = this.serviceContainer.get(IFormatterHelper); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; - } -} - -@injectable() -export class LinterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const linterManager = this.serviceContainer.get(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); - } -} - @injectable() export class TestFrameworkProductPathService extends BaseProductPathsService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { diff --git a/extensions/positron-python/src/client/common/installer/productService.ts b/extensions/positron-python/src/client/common/installer/productService.ts index b47ff49a691..1a5dca3aeb3 100644 --- a/extensions/positron-python/src/client/common/installer/productService.ts +++ b/extensions/positron-python/src/client/common/installer/productService.ts @@ -12,19 +12,8 @@ export class ProductService implements IProductService { private ProductTypes = new Map(); constructor() { - this.ProductTypes.set(Product.bandit, ProductType.Linter); - this.ProductTypes.set(Product.flake8, ProductType.Linter); - this.ProductTypes.set(Product.mypy, ProductType.Linter); - this.ProductTypes.set(Product.pycodestyle, ProductType.Linter); - this.ProductTypes.set(Product.prospector, ProductType.Linter); - this.ProductTypes.set(Product.pydocstyle, ProductType.Linter); - this.ProductTypes.set(Product.pylama, ProductType.Linter); - this.ProductTypes.set(Product.pylint, ProductType.Linter); this.ProductTypes.set(Product.pytest, ProductType.TestFramework); this.ProductTypes.set(Product.unittest, ProductType.TestFramework); - this.ProductTypes.set(Product.autopep8, ProductType.Formatter); - this.ProductTypes.set(Product.black, ProductType.Formatter); - this.ProductTypes.set(Product.yapf, ProductType.Formatter); this.ProductTypes.set(Product.ipykernel, ProductType.DataScience); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); diff --git a/extensions/positron-python/src/client/common/installer/serviceRegistry.ts b/extensions/positron-python/src/client/common/installer/serviceRegistry.ts index c262c757171..d4d8a05c3a4 100644 --- a/extensions/positron-python/src/client/common/installer/serviceRegistry.ts +++ b/extensions/positron-python/src/client/common/installer/serviceRegistry.ts @@ -9,12 +9,7 @@ import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { - DataScienceProductPathService, - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from './productPath'; +import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; @@ -25,12 +20,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); serviceManager.addSingleton(IProductService, ProductService); - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton( IProductPathService, TestFrameworkProductPathService, diff --git a/extensions/positron-python/src/client/common/platform/fs-paths.ts b/extensions/positron-python/src/client/common/platform/fs-paths.ts index 2d46fca9852..17df7507f7d 100644 --- a/extensions/positron-python/src/client/common/platform/fs-paths.ts +++ b/extensions/positron-python/src/client/common/platform/fs-paths.ts @@ -3,6 +3,7 @@ import * as nodepath from 'path'; import { getSearchPathEnvVarNames } from '../utils/exec'; +import * as fs from 'fs-extra'; import { getOSType, OSType } from '../utils/platform'; import { IExecutables, IFileSystemPaths, IFileSystemPathUtils } from './types'; @@ -170,3 +171,22 @@ export function isParentPath(filePath: string, parentPath: string): boolean { export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } + +export async function copyFile(src: string, dest: string): Promise { + const destDir = nodepath.dirname(dest); + if (!(await fs.pathExists(destDir))) { + await fs.mkdirp(destDir); + } + + await fs.copy(src, dest, { + overwrite: true, + }); +} + +export function pathExists(absPath: string): Promise { + return fs.pathExists(absPath); +} + +export function createFile(filename: string): Promise { + return fs.createFile(filename); +} diff --git a/extensions/positron-python/src/client/common/process/proc.ts b/extensions/positron-python/src/client/common/process/proc.ts index 0ac610e3eac..18add7daf6f 100644 --- a/extensions/positron-python/src/client/common/process/proc.ts +++ b/extensions/positron-python/src/client/common/process/proc.ts @@ -40,13 +40,15 @@ export class ProcessService extends EventEmitter implements IProcessService { } public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { - const result = execObservable(file, args, options, this.env, this.processesToKill); + const execOptions = { ...options, doNotLog: true }; + const result = execObservable(file, args, execOptions, 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.env, this.processesToKill); + const execOptions = { ...options, doNotLog: true }; + const promise = plainExec(file, args, execOptions, this.env, this.processesToKill); this.emit('exec', file, args, options); return promise; } @@ -54,7 +56,8 @@ export class ProcessService extends EventEmitter implements IProcessService { public shellExec(command: string, options: ShellOptions = {}): Promise> { this.emit('exec', command, undefined, options); const disposables = new Set(); - return shellExec(command, options, this.env, disposables).finally(() => { + const shellOptions = { ...options, doNotLog: true }; + return shellExec(command, shellOptions, this.env, disposables).finally(() => { // Ensure the process we started is cleaned up. disposables.forEach((p) => { try { diff --git a/extensions/positron-python/src/client/common/process/rawProcessApis.ts b/extensions/positron-python/src/client/common/process/rawProcessApis.ts index 23c2f3253bb..1e1b742a031 100644 --- a/extensions/positron-python/src/client/common/process/rawProcessApis.ts +++ b/extensions/positron-python/src/client/common/process/rawProcessApis.ts @@ -12,6 +12,8 @@ import { ExecutionResult, ObservableExecutionResult, Output, ShellOptions, Spawn import { noop } from '../utils/misc'; import { decodeBuffer } from './decoder'; import { traceVerbose } from '../../logging'; +import { WorkspaceService } from '../application/workspace'; +import { ProcessLogger } from './logger'; const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; @@ -49,12 +51,16 @@ function getDefaultOptions(options: T, de export function shellExec( command: string, - options: ShellOptions = {}, + options: ShellOptions & { doNotLog?: boolean } = {}, defaultEnv?: EnvironmentVariables, disposables?: Set, ): Promise> { const shellOptions = getDefaultOptions(options, defaultEnv); traceVerbose(`Shell Exec: ${command} with options: ${JSON.stringify(shellOptions, null, 4)}`); + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(command, undefined, shellOptions); + } return new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const callback = (e: any, stdout: any, stderr: any) => { @@ -73,7 +79,11 @@ export function shellExec( const disposable: IDisposable = { dispose: () => { if (!proc.killed) { - proc.kill(); + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } } }, }; @@ -86,12 +96,16 @@ export function shellExec( export function plainExec( file: string, args: string[], - options: SpawnOptions = {}, + options: SpawnOptions & { doNotLog?: boolean } = {}, defaultEnv?: EnvironmentVariables, disposables?: Set, ): Promise> { const spawnOptions = getDefaultOptions(options, defaultEnv); const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } const proc = spawn(file, args, spawnOptions); // Listen to these errors (unhandled errors in streams tears down the process). // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. @@ -101,7 +115,11 @@ export function plainExec( const disposable: IDisposable = { dispose: () => { if (!proc.killed) { - proc.kill(); + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } } }, }; @@ -184,12 +202,16 @@ function removeCondaRunMarkers(out: string) { export function execObservable( file: string, args: string[], - options: SpawnOptions = {}, + options: SpawnOptions & { doNotLog?: boolean } = {}, defaultEnv?: EnvironmentVariables, disposables?: Set, ): ObservableExecutionResult { const spawnOptions = getDefaultOptions(options, defaultEnv); const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } const proc = spawn(file, args, spawnOptions); let procExited = false; const disposable: IDisposable = { @@ -219,7 +241,11 @@ export function execObservable( internalDisposables.push( options.token.onCancellationRequested(() => { if (!procExited && !proc.killed) { - proc.kill(); + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } procExited = true; } }), @@ -279,6 +305,6 @@ export function killPid(pid: number): void { process.kill(pid); } } catch { - // Ignore. + traceVerbose('Unable to kill process with pid', pid); } } diff --git a/extensions/positron-python/src/client/common/serviceRegistry.ts b/extensions/positron-python/src/client/common/serviceRegistry.ts index be0559496ac..8c872c3113b 100644 --- a/extensions/positron-python/src/client/common/serviceRegistry.ts +++ b/extensions/positron-python/src/client/common/serviceRegistry.ts @@ -5,7 +5,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -51,7 +50,6 @@ import { import { WorkspaceService } from './application/workspace'; import { ConfigurationService } from './configuration/service'; import { PipEnvExecutionPath } from './configuration/executionSettings/pipEnvExecution'; -import { EditorUtils } from './editor'; import { ExperimentService } from './experiments/service'; import { ProductInstaller } from './installer/productInstaller'; import { InterpreterPathService } from './interpreterPathService'; @@ -130,7 +128,6 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); serviceManager.addSingleton(ILanguageService, LanguageService); serviceManager.addSingleton(IBrowserService, BrowserService); - serviceManager.addSingleton(IEditorUtils, EditorUtils); serviceManager.addSingleton(ITerminalActivator, TerminalActivator); serviceManager.addSingleton( ITerminalActivationHandler, diff --git a/extensions/positron-python/src/client/common/types.ts b/extensions/positron-python/src/client/common/types.ts index ec72c5f4b57..09aef5e4707 100644 --- a/extensions/positron-python/src/client/common/types.ts +++ b/extensions/positron-python/src/client/common/types.ts @@ -8,7 +8,6 @@ import { CancellationToken, ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, @@ -17,7 +16,6 @@ import { Memento, LogOutputChannel, Uri, - WorkspaceEdit, OutputChannel, MessageOptions, } from 'vscode'; @@ -87,29 +85,14 @@ export enum ProductInstallStatus { } export enum ProductType { - Linter = 'Linter', - Formatter = 'Formatter', TestFramework = 'TestFramework', - RefactoringLibrary = 'RefactoringLibrary', DataScience = 'DataScience', Python = 'Python', } export enum Product { pytest = 1, - pylint = 3, - flake8 = 4, - pycodestyle = 5, - pylama = 6, - prospector = 7, - pydocstyle = 8, - yapf = 9, - autopep8 = 10, - mypy = 11, unittest = 12, - isort = 15, - black = 16, - bandit = 17, ipykernel = 19, tensorboard = 24, torchProfilerInstallName = 25, @@ -191,12 +174,9 @@ export interface IPythonSettings { readonly pipenvPath: string; readonly poetryPath: string; readonly devOptions: string[]; - readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; - readonly sortImports: ISortImportSettings; readonly envFile: string; readonly globalModuleInstallation: boolean; readonly experiments: IExperiments; @@ -214,81 +194,11 @@ export interface IPythonSettings { export interface ITensorBoardSettings { logDirectory: string | undefined; } -export interface ISortImportSettings { - readonly path: string; - readonly args: string[]; -} - -export interface IPylintCategorySeverity { - readonly convention: DiagnosticSeverity; - readonly refactor: DiagnosticSeverity; - readonly warning: DiagnosticSeverity; - readonly error: DiagnosticSeverity; - readonly fatal: DiagnosticSeverity; -} -export interface IPycodestyleCategorySeverity { - readonly W: DiagnosticSeverity; - readonly E: DiagnosticSeverity; -} -export interface Flake8CategorySeverity { - readonly F: DiagnosticSeverity; - readonly E: DiagnosticSeverity; - readonly W: DiagnosticSeverity; -} -export interface IMypyCategorySeverity { - readonly error: DiagnosticSeverity; - readonly note: DiagnosticSeverity; -} export interface IInterpreterSettings { infoVisibility: 'never' | 'onPythonRelated' | 'always'; } -export interface ILintingSettings { - readonly enabled: boolean; - readonly ignorePatterns: string[]; - readonly prospectorEnabled: boolean; - readonly prospectorArgs: string[]; - readonly pylintEnabled: boolean; - readonly pylintArgs: string[]; - readonly pycodestyleEnabled: boolean; - readonly pycodestyleArgs: string[]; - readonly pylamaEnabled: boolean; - readonly pylamaArgs: string[]; - readonly flake8Enabled: boolean; - readonly flake8Args: string[]; - readonly pydocstyleEnabled: boolean; - readonly pydocstyleArgs: string[]; - readonly lintOnSave: boolean; - readonly maxNumberOfProblems: number; - readonly pylintCategorySeverity: IPylintCategorySeverity; - readonly pycodestyleCategorySeverity: IPycodestyleCategorySeverity; - readonly flake8CategorySeverity: Flake8CategorySeverity; - readonly mypyCategorySeverity: IMypyCategorySeverity; - cwd?: string; - prospectorPath: string; - pylintPath: string; - pycodestylePath: string; - pylamaPath: string; - flake8Path: string; - pydocstylePath: string; - mypyEnabled: boolean; - mypyArgs: string[]; - mypyPath: string; - banditEnabled: boolean; - banditArgs: string[]; - banditPath: string; -} -export interface IFormattingSettings { - readonly provider: string; - autopep8Path: string; - readonly autopep8Args: string[]; - blackPath: string; - readonly blackArgs: string[]; - yapfPath: string; - readonly yapfArgs: string[]; -} - export interface ITerminalSettings { readonly executeInFileDir: boolean; readonly focusAfterLaunch: boolean; @@ -411,11 +321,6 @@ export interface IBrowserService { launch(url: string): void; } -export const IEditorUtils = Symbol('IEditorUtils'); -export interface IEditorUtils { - getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; -} - /** * Stores hash formats */ diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index bc32c1078ca..95252b361c7 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -39,9 +39,6 @@ export namespace Diagnostics { 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?', ); export const updateSettings = l10n.t('Yes, update settings'); - export const checkIsort5UpgradeGuide = l10n.t( - 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.', - ); export const pylanceDefaultMessage = l10n.t( "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); @@ -64,6 +61,7 @@ export namespace Common { export const noIWillDoItLater = l10n.t('No, I will do it later'); export const notNow = l10n.t('Not now'); export const doNotShowAgain = l10n.t("Don't show again"); + export const editSomething = l10n.t('Edit {0}'); export const reload = l10n.t('Reload'); export const moreInfo = l10n.t('More Info'); export const learnMore = l10n.t('Learn more'); @@ -199,7 +197,12 @@ export namespace Interpreters { export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); export const terminalEnvVarCollectionPrompt = l10n.t( - 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + '{0} environment was successfully activated, even though {1} may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + ); + export const terminalDeactivateProgress = l10n.t('Editing {0}...'); + export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); + export const terminalDeactivatePrompt = l10n.t( + 'Deactivating virtual environments may not work by default. To make it work, edit your "{0}" and then restart your shell. [Learn more](https://aka.ms/AAmx2ft).', ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', @@ -509,38 +512,3 @@ export namespace CreateEnv { export const disableCheckWorkspace = l10n.t('Disable (Workspace)'); } } - -export namespace ToolsExtensions { - export const flake8PromptMessage = l10n.t( - 'Use the Flake8 extension to enable easier configuration and new features such as quick fixes.', - ); - export const pylintPromptMessage = l10n.t( - 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', - ); - export const isortPromptMessage = l10n.t( - 'To use sort imports, install the isort extension. It provides easier configuration and new features such as code actions.', - ); - export const installPylintExtension = l10n.t('Install Pylint extension'); - export const installFlake8Extension = l10n.t('Install Flake8 extension'); - export const installISortExtension = l10n.t('Install isort extension'); - - export const selectBlackFormatterPrompt = l10n.t( - 'You have the Black formatter extension installed, would you like to use that as the default formatter?', - ); - - export const selectAutopep8FormatterPrompt = l10n.t( - 'You have the Autopep8 formatter extension installed, would you like to use that as the default formatter?', - ); - - export const selectMultipleFormattersPrompt = l10n.t( - 'You have multiple formatters installed, would you like to select one as the default formatter?', - ); - - export const installBlackFormatterPrompt = l10n.t( - 'You triggered formatting with Black, would you like to install one of our new formatter extensions? This will also set it as the default formatter for Python.', - ); - - export const installAutopep8FormatterPrompt = l10n.t( - 'You triggered formatting with Autopep8, would you like to install one of our new formatter extension? This will also set it as the default formatter for Python.', - ); -} diff --git a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts index c761ff60fa6..f2d934d322f 100644 --- a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts +++ b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts @@ -54,6 +54,23 @@ export function showErrorMessage(message: string, ...items: any[]): Thenable< return window.showErrorMessage(message, ...items); } +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showWarningMessage(message: string, ...items: any[]): Thenable { + return window.showWarningMessage(message, ...items); +} + export function showInformationMessage(message: string, ...items: T[]): Thenable; export function showInformationMessage( message: string, diff --git a/extensions/positron-python/src/client/extensionActivation.ts b/extensions/positron-python/src/client/extensionActivation.ts index 807698f3ec2..c4b663fdba6 100644 --- a/extensions/positron-python/src/client/extensionActivation.ts +++ b/extensions/positron-python/src/client/extensionActivation.ts @@ -10,7 +10,7 @@ import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, PYTHON, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; +import { Commands, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; import { @@ -25,11 +25,8 @@ import { noop } from './common/utils/misc'; import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; -import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { TerminalProvider } from './providers/terminalProvider'; @@ -51,10 +48,10 @@ import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; -import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; +import { logAndNotifyOnLegacySettings } from './logging/settingLogs'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -110,7 +107,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): // See https://github.com/microsoft/vscode-python/issues/10454. async function activateLegacy(ext: ExtensionState): Promise { - const { context, legacyIOC } = ext; + const { legacyIOC } = ext; const { serviceManager, serviceContainer } = legacyIOC; // register "services" @@ -124,8 +121,6 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); // Feature specific registrations. unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); @@ -134,7 +129,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const extensions = serviceContainer.get(IExtensions); await setDefaultLanguageServer(extensions, serviceManager); - const configuration = serviceManager.get(IConfigurationService); // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. serviceContainer.get(IConfigurationService).getSettings().register(); @@ -165,20 +159,9 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceContainer.get(IApplicationDiagnostics).register(); serviceManager.get(ITerminalAutoActivation).register(); - const pythonSettings = configuration.getSettings(); serviceManager.get(ICodeExecutionManager).registerCommands(); - if ( - pythonSettings && - pythonSettings.formatting && - pythonSettings.formatting.provider !== 'internalConsole' - ) { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - disposables.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - disposables.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } - disposables.push(new ReplProvider(serviceContainer)); const terminalProvider = new TerminalProvider(serviceContainer); @@ -200,7 +183,7 @@ async function activateLegacy(ext: ExtensionState): Promise { ), ); - registerInstallFormatterPrompt(serviceContainer); + logAndNotifyOnLegacySettings(); registerCreateEnvironmentTriggers(disposables); initializePersistentStateForTriggers(ext.context); } diff --git a/extensions/positron-python/src/client/formatters/autoPep8Formatter.ts b/extensions/positron-python/src/client/formatters/autoPep8Formatter.ts deleted file mode 100644 index bf1285a60b5..00000000000 --- a/extensions/positron-python/src/client/formatters/autoPep8Formatter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class AutoPep8Formatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('autopep8', Product.autopep8, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = - Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const autoPep8Args = ['--diff']; - if (formatSelection) { - autoPep8Args.push( - ...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()], - ); - } - const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { - tool: 'autopep8', - hasCustomArgs, - formatSelection, - }); - return promise; - } -} diff --git a/extensions/positron-python/src/client/formatters/baseFormatter.ts b/extensions/positron-python/src/client/formatters/baseFormatter.ts deleted file mode 100644 index 64e7d15a3d4..00000000000 --- a/extensions/positron-python/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IDisposableRegistry, IInstaller, Product } from '../common/types'; -import { isNotebookCell } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; -import { IFormatterHelper } from './types'; -import { IInstallFormatterPrompt } from '../providers/prompts/types'; - -export abstract class BaseFormatter { - protected readonly workspace: IWorkspaceService; - private readonly helper: IFormatterHelper; - private errorShown: boolean = false; - - constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { - this.helper = serviceContainer.get(IFormatterHelper); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public abstract formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable; - protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { - if (path.basename(document.uri.fsPath) === document.uri.fsPath) { - return fallbackPath; - } - return path.dirname(document.fileName); - } - protected getWorkspaceUri(document: vscode.TextDocument) { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const folders = this.workspace.workspaceFolders; - if (Array.isArray(folders) && folders.length > 0) { - return folders[0].uri; - } - return vscode.Uri.file(__dirname); - } - protected async provideDocumentFormattingEdits( - document: vscode.TextDocument, - _options: vscode.FormattingOptions, - token: vscode.CancellationToken, - args: string[], - cwd?: string, - ): Promise { - if (typeof cwd !== 'string' || cwd.length === 0) { - cwd = this.getWorkspaceUri(document).fsPath; - } - - // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. - // However they don't support returning the diff of the formatted text when reading data from the input stream. - // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. - // Also, always create temp files for Notebook cells. - const tempFile = await this.createTempFile(document); - if (this.checkCancellation(document.fileName, tempFile, token)) { - return []; - } - - const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); - executionInfo.args.push(tempFile); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - const promise = pythonToolsExecutionService - .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) - .then((output) => output.stdout) - .then((data) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - return getTextEditsFromPatch(document.getText(), data); - }) - .catch((error) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - - this.handleError(this.Id, error, document.uri).catch(() => {}); - return [] as vscode.TextEdit[]; - }) - .then((edits) => { - this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); - return edits; - }); - - const appShell = this.serviceContainer.get(IApplicationShell); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - disposableRegistry.push(disposable); - return promise; - } - - protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - if (isNotInstalledError(error)) { - const prompt = this.serviceContainer.get(IInstallFormatterPrompt); - if (!(await prompt.showInstallFormatterPrompt(resource))) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled && !this.errorShown) { - traceError( - `\nPlease install '${this.Id}' into your environment.`, - "\nIf you don't want to use it you can turn it off or use another formatter in the settings.", - ); - this.errorShown = true; - } - } - } - - traceError(`Formatting with ${this.Id} failed:\n${error}`); - } - - /** - * Always create a temporary file when formatting notebook cells. - * This is because there is no physical file associated with notebook cells (they are all virtual). - */ - private async createTempFile(document: vscode.TextDocument): Promise { - const fs = this.serviceContainer.get(IFileSystem); - return document.isDirty || isNotebookCell(document) - ? getTempFileWithDocumentContents(document, fs) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise { - if (originalFile !== tempFile) { - const fs = this.serviceContainer.get(IFileSystem); - return fs.deleteFile(tempFile); - } - return Promise.resolve(); - } - - private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { - if (token && token.isCancellationRequested) { - this.deleteTempFile(originalFile, tempFile).ignoreErrors(); - return true; - } - return false; - } -} diff --git a/extensions/positron-python/src/client/formatters/blackFormatter.ts b/extensions/positron-python/src/client/formatters/blackFormatter.ts deleted file mode 100644 index 0a8109e163e..00000000000 --- a/extensions/positron-python/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public async formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Promise { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - if (formatSelection) { - const shell = this.serviceContainer.get(IApplicationShell); - // Black does not support partial formatting on purpose. - shell - .showErrorMessage(vscode.l10n.t('Black does not support the "Format Selection" command')) - .then(noop, noop); - return []; - } - - const blackArgs = ['--diff', '--quiet']; - - if (path.extname(document.fileName) === '.pyi') { - blackArgs.push('--pyi'); - } - - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/extensions/positron-python/src/client/formatters/dummyFormatter.ts b/extensions/positron-python/src/client/formatters/dummyFormatter.ts deleted file mode 100644 index b4fdba9fbc0..00000000000 --- a/extensions/positron-python/src/client/formatters/dummyFormatter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseFormatter } from './baseFormatter'; - -export class DummyFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('none', Product.yapf, serviceContainer); - } - - public formatDocument( - _document: vscode.TextDocument, - _options: vscode.FormattingOptions, - _token: vscode.CancellationToken, - _range?: vscode.Range, - ): Thenable { - return Promise.resolve([]); - } -} diff --git a/extensions/positron-python/src/client/formatters/helper.ts b/extensions/positron-python/src/client/formatters/helper.ts deleted file mode 100644 index ac305b51e78..00000000000 --- a/extensions/positron-python/src/client/formatters/helper.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { ExecutionInfo, IConfigurationService, IFormattingSettings, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; - -@injectable() -export class FormatterHelper implements IFormatterHelper { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - public translateToId(formatter: Product): FormatterId { - switch (formatter) { - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; - case Product.yapf: - return 'yapf'; - default: { - throw new Error(`Unrecognized Formatter '${formatter}'`); - } - } - } - public getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames { - const id = this.translateToId(formatter); - return { - argsName: `${id}Args` as keyof IFormattingSettings, - pathName: `${id}Path` as keyof IFormattingSettings, - }; - } - public getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo { - const settings = this.serviceContainer.get(IConfigurationService).getSettings(resource); - const names = this.getSettingsPropertyNames(formatter); - - const execPath = settings.formatting[names.pathName] as string; - let args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - args = args.concat(customArgs); - - let moduleName: string | undefined; - - // If path information is not available, then treat it as a module, - if (path.basename(execPath) === execPath) { - moduleName = execPath; - } - - return { execPath, moduleName, args, product: formatter }; - } -} diff --git a/extensions/positron-python/src/client/formatters/serviceRegistry.ts b/extensions/positron-python/src/client/formatters/serviceRegistry.ts deleted file mode 100644 index 196e6c806b5..00000000000 --- a/extensions/positron-python/src/client/formatters/serviceRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IServiceManager } from '../ioc/types'; -import { FormatterHelper } from './helper'; -import { IFormatterHelper } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IFormatterHelper, FormatterHelper); -} diff --git a/extensions/positron-python/src/client/formatters/types.ts b/extensions/positron-python/src/client/formatters/types.ts deleted file mode 100644 index 7f4bcf5b752..00000000000 --- a/extensions/positron-python/src/client/formatters/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; - -export const IFormatterHelper = Symbol('IFormatterHelper'); - -export type FormatterId = 'autopep8' | 'black' | 'yapf'; - -export type FormatterSettingsPropertyNames = { - argsName: keyof IFormattingSettings; - pathName: keyof IFormattingSettings; -}; - -export interface IFormatterHelper { - translateToId(formatter: Product): FormatterId; - getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; - getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo; -} diff --git a/extensions/positron-python/src/client/formatters/yapfFormatter.ts b/extensions/positron-python/src/client/formatters/yapfFormatter.ts deleted file mode 100644 index 08729a97694..00000000000 --- a/extensions/positron-python/src/client/formatters/yapfFormatter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as vscode from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class YapfFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('yapf', Product.yapf, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const yapfArgs = ['--diff']; - if (formatSelection && range !== undefined) { - yapfArgs.push(...['--lines', `${range.start.line + 1}-${range.end.line + 1}`]); - } - // Yapf starts looking for config file starting from the file path. - const fallbarFolder = this.getWorkspaceUri(document).fsPath; - const cwd = this.getDocumentPath(document, fallbarFolder); - const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/extensions/positron-python/src/client/interpreter/activation/service.ts b/extensions/positron-python/src/client/interpreter/activation/service.ts index 02d621c0ccd..f97545a5823 100644 --- a/extensions/positron-python/src/client/interpreter/activation/service.ts +++ b/extensions/positron-python/src/client/interpreter/activation/service.ts @@ -329,7 +329,15 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } if (result.stderr) { if (returnedEnv) { - traceWarn('Got env variables but with errors', result.stderr); + traceWarn('Got env variables but with errors', result.stderr, returnedEnv); + if ( + result.stderr.includes('running scripts is disabled') || + result.stderr.includes('FullyQualifiedErrorId : UnauthorizedAccess') + ) { + throw new Error( + `Skipping returned result when powershell execution is disabled, stderr ${result.stderr} for ${command}`, + ); + } } else { throw new Error(`StdErr from ShellExec, ${result.stderr} for ${command}`); } diff --git a/extensions/positron-python/src/client/interpreter/activation/types.ts b/extensions/positron-python/src/client/interpreter/activation/types.ts index 2b364cbeb86..e00ef9b62b3 100644 --- a/extensions/positron-python/src/client/interpreter/activation/types.ts +++ b/extensions/positron-python/src/client/interpreter/activation/types.ts @@ -21,11 +21,3 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } - -export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); -export interface ITerminalEnvVarCollectionService { - /** - * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. - */ - isTerminalPromptSetCorrectly(resource?: Resource): boolean; -} diff --git a/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts b/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts index 0631bb594bf..2b1c57c4531 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -234,6 +234,10 @@ export function getEnvLocationHeuristic(environment: PythonEnvironment, workspac * Compare 2 environment types: return 0 if they are the same, -1 if a comes before b, 1 otherwise. */ function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment): number { + if (!a.type && !b.type) { + // Return 0 if two global interpreters are being compared. + return 0; + } const envTypeByPriority = getPrioritizedEnvironmentType(); return Math.sign(envTypeByPriority.indexOf(a.envType) - envTypeByPriority.indexOf(b.envType)); } diff --git a/extensions/positron-python/src/client/interpreter/serviceRegistry.ts b/extensions/positron-python/src/client/interpreter/serviceRegistry.ts index 018e7abfdc4..422776bd5e4 100644 --- a/extensions/positron-python/src/client/interpreter/serviceRegistry.ts +++ b/extensions/positron-python/src/client/interpreter/serviceRegistry.ts @@ -6,9 +6,7 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; -import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; +import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -110,13 +108,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - ITerminalEnvVarCollectionService, - TerminalEnvVarCollectionService, - ); - serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalEnvVarCollectionPrompt, - ); } diff --git a/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts index 468c2dc72a0..b4dcfe36e09 100644 --- a/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts +++ b/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -91,7 +91,10 @@ export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { } if (process.env.VSCODE_CLI !== '1') { // We only want to select the interpreter if VS Code was launched from the command line. - traceVerbose('VS Code was not launched from the command line, not selecting activated interpreter'); + traceVerbose( + 'VS Code was not launched from the command line, not selecting activated interpreter', + JSON.stringify(process.env, undefined, 4), + ); return undefined; } const prefix = await this.getPrefixOfSelectedActivatedEnv(); diff --git a/extensions/positron-python/src/client/linters/bandit.ts b/extensions/positron-python/src/client/linters/bandit.ts deleted file mode 100644 index bbc8836bfc6..00000000000 --- a/extensions/positron-python/src/client/linters/bandit.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -const severityMapping: Record = { - LOW: LintMessageSeverity.Information, - MEDIUM: LintMessageSeverity.Warning, - HIGH: LintMessageSeverity.Error, -}; - -export const BANDIT_REGEX = - '(?\\d+),(?(col)?(\\d+)?),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; - -export class Bandit extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.bandit, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - // View all errors in bandit <= 1.5.1 (https://github.com/PyCQA/bandit/issues/371) - const messages = await this.run([document.uri.fsPath], document, cancellation, BANDIT_REGEX); - - messages.forEach((msg) => { - msg.severity = severityMapping[msg.type]; - }); - return messages; - } -} diff --git a/extensions/positron-python/src/client/linters/baseLinter.ts b/extensions/positron-python/src/client/linters/baseLinter.ts deleted file mode 100644 index bb24bee1637..00000000000 --- a/extensions/positron-python/src/client/linters/baseLinter.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { splitLines } from '../common/stringUtils'; -import { - ExecutionInfo, - Flake8CategorySeverity, - IConfigurationService, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, - IPythonSettings, - Product, -} from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { ErrorHandler } from './errorHandlers/errorHandler'; -import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from './types'; - -const namedRegexp = require('named-js-regexp'); -// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) -// Allow codes with more than one letter (i.e. ABC123) -const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; - -interface IRegexGroup { - line: number; - column: number; - code: string; - message: string; - type: string; -} - -function matchNamedRegEx(data: string, regex: string): IRegexGroup | undefined { - const compiledRegexp = namedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups(); - } - - return undefined; -} - -export function parseLine(line: string, regex: string, linterID: LinterId, colOffset = 0): ILintMessage | undefined { - const match = matchNamedRegEx(line, regex)!; - if (!match) { - return undefined; - } - - match.line = Number(match.line); - - match.column = Number(match.column); - - return { - code: match.code, - message: match.message, - column: Number.isNaN(match.column) || match.column <= 0 ? 0 : match.column - colOffset, - line: match.line, - type: match.type, - provider: linterID, - }; -} - -export abstract class BaseLinter implements ILinter { - protected readonly configService: IConfigurationService; - - private errorHandler: ErrorHandler; - - private _pythonSettings!: IPythonSettings; - - private _info: ILinterInfo; - - private workspace: IWorkspaceService; - - protected get pythonSettings(): IPythonSettings { - return this._pythonSettings; - } - - constructor( - product: Product, - protected readonly serviceContainer: IServiceContainer, - protected readonly columnOffset = 0, - ) { - this._info = serviceContainer.get(ILinterManager).getLinterInfo(product); - this.errorHandler = new ErrorHandler(this.info.product, serviceContainer); - this.configService = serviceContainer.get(IConfigurationService); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public get info(): ILinterInfo { - return this._info; - } - - public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { - this._pythonSettings = this.configService.getSettings(document.uri); - return this.runLinter(document, cancellation); - } - - protected getWorkspaceRootPath(document: vscode.TextDocument): string { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = - workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; - return typeof workspaceRootPath === 'string' ? workspaceRootPath : path.dirname(document.uri.fsPath); - } - - protected getWorkingDirectoryPath(document: vscode.TextDocument): string { - return this._pythonSettings.linting.cwd || this.getWorkspaceRootPath(document); - } - - protected abstract runLinter( - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - ): Promise; - - // eslint-disable-next-line class-methods-use-this - protected parseMessagesSeverity( - error: string, - categorySeverity: - | Flake8CategorySeverity - | IMypyCategorySeverity - | IPycodestyleCategorySeverity - | IPylintCategorySeverity, - ): LintMessageSeverity { - const severity = error as keyof typeof categorySeverity; - - if (categorySeverity[severity]) { - const severityName = categorySeverity[severity]; - switch (severityName) { - case 'Error': - return LintMessageSeverity.Error; - case 'Hint': - return LintMessageSeverity.Hint; - case 'Information': - return LintMessageSeverity.Information; - case 'Warning': - return LintMessageSeverity.Warning; - default: { - if (LintMessageSeverity[severityName]) { - return (LintMessageSeverity[severityName] as unknown) as LintMessageSeverity; - } - } - } - } - return LintMessageSeverity.Information; - } - - protected async run( - args: string[], - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - regEx: string = REGEX, - ): Promise { - if (!this.info.isEnabled(document.uri)) { - return []; - } - const executionInfo = this.info.getExecutionInfo(args, document.uri); - const cwd = this.getWorkingDirectoryPath(document); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - try { - const result = await pythonToolsExecutionService.execForLinter( - executionInfo, - { cwd, token: cancellation, mergeStdOutErr: false }, - document.uri, - ); - this.displayLinterResultHeader(result.stdout); - return await this.parseMessages(result.stdout, document, cancellation, regEx); - } catch (error) { - await this.handleError(error as Error, document.uri, executionInfo); - return []; - } - } - - protected async parseMessages( - output: string, - _document: vscode.TextDocument, - _token: vscode.CancellationToken, - regEx: string, - ): Promise { - const outputLines = splitLines(output, { removeEmptyEntries: false, trim: false }); - return this.parseLines(outputLines, regEx); - } - - protected async handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise { - if (isTestExecution()) { - this.errorHandler.handleError(error, resource, execInfo).ignoreErrors(); - } else { - this.errorHandler - .handleError(error, resource, execInfo) - .catch((ex) => traceError('Error in errorHandler.handleError', ex)) - .ignoreErrors(); - } - } - - private parseLine(line: string, regEx: string): ILintMessage | undefined { - return parseLine(line, regEx, this.info.id, this.columnOffset); - } - - private parseLines(outputLines: string[], regEx: string): ILintMessage[] { - const messages: ILintMessage[] = []; - for (const line of outputLines) { - try { - const msg = this.parseLine(line, regEx); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } catch (ex) { - traceError(`Linter '${this.info.id}' failed to parse the line '${line}.`, ex); - } - } - return messages; - } - - private displayLinterResultHeader(data: string) { - traceLog(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}\n`); - traceLog(data); - } -} diff --git a/extensions/positron-python/src/client/linters/constants.ts b/extensions/positron-python/src/client/linters/constants.ts deleted file mode 100644 index 27b7c80db7f..00000000000 --- a/extensions/positron-python/src/client/linters/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Product } from '../common/types'; -import { LinterId } from './types'; - -// All supported linters must be in this map. -export const LINTERID_BY_PRODUCT = new Map([ - [Product.bandit, LinterId.Bandit], - [Product.flake8, LinterId.Flake8], - [Product.pylint, LinterId.PyLint], - [Product.mypy, LinterId.MyPy], - [Product.pycodestyle, LinterId.PyCodeStyle], - [Product.prospector, LinterId.Prospector], - [Product.pydocstyle, LinterId.PyDocStyle], - [Product.pylama, LinterId.PyLama], -]); diff --git a/extensions/positron-python/src/client/linters/errorHandlers/baseErrorHandler.ts b/extensions/positron-python/src/client/linters/errorHandlers/baseErrorHandler.ts deleted file mode 100644 index 16c5e93ae01..00000000000 --- a/extensions/positron-python/src/client/linters/errorHandlers/baseErrorHandler.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IInstaller, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; - -export abstract class BaseErrorHandler implements IErrorHandler { - protected installer: IInstaller; - - private handler?: IErrorHandler; - - constructor(protected product: Product, protected serviceContainer: IServiceContainer) { - this.installer = this.serviceContainer.get(IInstaller); - } - - protected get nextHandler(): IErrorHandler | undefined { - return this.handler; - } - - public setNextHandler(handler: IErrorHandler): void { - this.handler = handler; - } - - public abstract handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; -} diff --git a/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts b/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts deleted file mode 100644 index af28dd61c3a..00000000000 --- a/extensions/positron-python/src/client/linters/errorHandlers/errorHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Uri } from 'vscode'; -import { ExecutionInfo, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; -import { StandardErrorHandler } from './standard'; - -export class ErrorHandler implements IErrorHandler { - private handler: BaseErrorHandler; - - constructor(product: Product, serviceContainer: IServiceContainer) { - this.handler = new StandardErrorHandler(product, serviceContainer); - } - - public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - return this.handler.handleError(error, resource, execInfo); - } -} diff --git a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts deleted file mode 100644 index 6367da7abe4..00000000000 --- a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { l10n, Uri } from 'vscode'; -import { IApplicationShell } from '../../common/application/types'; -import { ExecutionInfo, ILogOutputChannel } from '../../common/types'; -import { traceError, traceLog } from '../../logging'; -import { ILinterManager, LinterId } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class StandardErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - if ( - typeof error === 'string' && - (error as string).includes("OSError: [Errno 2] No such file or directory: '/") - ) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : Promise.resolve(false); - } - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - - traceError(`There was an error in running the linter ${info.id}`, error); - if (info.id === LinterId.PyLint) { - traceError('Support for "pylint" is moved to ms-python.pylint extension.'); - traceError( - 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.pylint', - ); - } else if (info.id === LinterId.Flake8) { - traceError('Support for "flake8" is moved to ms-python.flake8 extension.'); - traceError( - 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.flake8', - ); - } else if (info.id === LinterId.MyPy) { - traceError('Support for "mypy" is moved to ms-python.mypy-type-checker extension.'); - traceError( - 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker', - ); - } - traceError(`If the error is due to missing ${info.id}, please install ${info.id} using pip manually.`); - traceError('Learn more here: https://aka.ms/AAlgvkb'); - traceLog(`Linting with ${info.id} failed.`); - traceLog(error.toString()); - - this.displayLinterError(info.id).ignoreErrors(); - return true; - } - - private async displayLinterError(linterId: LinterId) { - const message = l10n.t("There was an error in running the linter '{0}'", linterId); - const appShell = this.serviceContainer.get(IApplicationShell); - const outputChannel = this.serviceContainer.get(ILogOutputChannel); - const action = await appShell.showErrorMessage(message, 'View Errors'); - if (action === 'View Errors') { - outputChannel.show(); - } - } -} diff --git a/extensions/positron-python/src/client/linters/flake8.ts b/extensions/positron-python/src/client/linters/flake8.ts deleted file mode 100644 index e79d0915874..00000000000 --- a/extensions/positron-python/src/client/linters/flake8.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { isExtensionEnabled } from './prompts/common'; -import { FLAKE8_EXTENSION } from './prompts/flake8Prompt'; -import { IToolsExtensionPrompt } from './prompts/types'; -import { ILintMessage } from './types'; - -const COLUMN_OFF_SET = 1; - -export class Flake8 extends BaseLinter { - constructor(serviceContainer: IServiceContainer, private readonly prompt: IToolsExtensionPrompt) { - super(Product.flake8, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - await this.prompt.showPrompt(); - - if (isExtensionEnabled(this.serviceContainer, FLAKE8_EXTENSION)) { - traceLog( - 'LINTING: Skipping linting from Python extension, since Flake8 extension is installed and enabled.', - ); - return []; - } - - const messages = await this.run([document.uri.fsPath], document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.flake8CategorySeverity); - // flake8 uses 0th line for some file-wide problems - // but diagnostics expects positive line numbers. - if (msg.line === 0) { - msg.line = 1; - } - }); - return messages; - } -} diff --git a/extensions/positron-python/src/client/linters/linterInfo.ts b/extensions/positron-python/src/client/linters/linterInfo.ts deleted file mode 100644 index 321f23b0f30..00000000000 --- a/extensions/positron-python/src/client/linters/linterInfo.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { Uri } from 'vscode'; -import { linterScript } from '../common/process/internal/scripts'; -import { ExecutionInfo, IConfigurationService, ILintingSettings, Product } from '../common/types'; -import { ILinterInfo, LinterId } from './types'; - -export class LinterInfo implements ILinterInfo { - private _id: LinterId; - - private _product: Product; - - private _configFileNames: string[]; - - constructor( - product: Product, - id: LinterId, - protected configService: IConfigurationService, - configFileNames: string[] = [], - ) { - this._product = product; - this._id = id; - this._configFileNames = configFileNames; - } - - public get id(): LinterId { - return this._id; - } - - public get product(): Product { - return this._product; - } - - public get pathSettingName(): string { - return `${this.id}Path`; - } - - public get argsSettingName(): string { - return `${this.id}Args`; - } - - public get enabledSettingName(): string { - return `${this.id}Enabled`; - } - - public get configFileNames(): string[] { - return this._configFileNames; - } - - public async enableAsync(enabled: boolean, resource?: Uri): Promise { - return this.configService.updateSetting(`linting.${this.enabledSettingName}`, enabled, resource); - } - - public isEnabled(resource?: Uri): boolean { - const settings = this.configService.getSettings(resource); - const name = this.enabledSettingName as keyof ILintingSettings; - return settings.linting[name] as boolean; - } - - public pathName(resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const name = this.pathSettingName as keyof ILintingSettings; - return settings.linting[name] as string; - } - - public linterArgs(resource?: Uri): string[] { - const settings = this.configService.getSettings(resource); - const name = this.argsSettingName as keyof ILintingSettings; - const args = settings.linting[name]; - return Array.isArray(args) ? (args as string[]) : []; - } - - public getExecutionInfo(customArgs: string[], resource?: Uri): ExecutionInfo { - const execPath = this.pathName(resource); - const args = this.linterArgs(resource).concat(customArgs); - const script = linterScript(); - if (path.basename(execPath) === execPath) { - return { - execPath: undefined, - args: [script, '-m', this.id, ...args], - product: this.product, - moduleName: execPath, - }; - } - return { - execPath, - moduleName: this.id, - args: [script, '-p', this.id, execPath, ...args], - product: this.product, - }; - } -} diff --git a/extensions/positron-python/src/client/linters/linterManager.ts b/extensions/positron-python/src/client/linters/linterManager.ts deleted file mode 100644 index 72c92aa1c77..00000000000 --- a/extensions/positron-python/src/client/linters/linterManager.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable max-classes-per-file */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { CancellationToken, TextDocument, Uri } from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { Bandit } from './bandit'; -import { Flake8 } from './flake8'; -import { LinterInfo } from './linterInfo'; -import { MyPy } from './mypy'; -import { getOrCreateFlake8Prompt } from './prompts/flake8Prompt'; -import { getOrCreatePylintPrompt } from './prompts/pylintPrompt'; -import { Prospector } from './prospector'; -import { Pycodestyle } from './pycodestyle'; -import { PyDocStyle } from './pydocstyle'; -import { PyLama } from './pylama'; -import { Pylint } from './pylint'; -import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId } from './types'; - -class DisabledLinter implements ILinter { - constructor(private configService: IConfigurationService) {} - - public get info() { - return new LinterInfo(Product.pylint, LinterId.PyLint, this.configService); - } - - // eslint-disable-next-line class-methods-use-this - public async lint(_document: TextDocument, _cancellation: CancellationToken): Promise { - return []; - } -} - -@injectable() -export class LinterManager implements ILinterManager { - protected linters: ILinterInfo[]; - - constructor(@inject(IConfigurationService) private configService: IConfigurationService) { - // Note that we use unit tests to ensure all the linters are here. - this.linters = [ - new LinterInfo(Product.bandit, LinterId.Bandit, this.configService), - new LinterInfo(Product.flake8, LinterId.Flake8, this.configService), - new LinterInfo(Product.pylint, LinterId.PyLint, this.configService, ['pylintrc', '.pylintrc']), - new LinterInfo(Product.mypy, LinterId.MyPy, this.configService), - new LinterInfo(Product.pycodestyle, LinterId.PyCodeStyle, this.configService), - new LinterInfo(Product.prospector, LinterId.Prospector, this.configService), - new LinterInfo(Product.pydocstyle, LinterId.PyDocStyle, this.configService), - new LinterInfo(Product.pylama, LinterId.PyLama, this.configService), - ]; - } - - public getAllLinterInfos(): ILinterInfo[] { - return this.linters; - } - - public getLinterInfo(product: Product): ILinterInfo { - const x = this.linters.findIndex((value, _index, _obj) => value.product === product); - if (x >= 0) { - return this.linters[x]; - } - throw new Error(`Invalid linter '${Product[product]}'`); - } - - public async isLintingEnabled(resource?: Uri): Promise { - const settings = this.configService.getSettings(resource); - const activeLintersPresent = await this.getActiveLinters(resource); - return settings.linting.enabled && activeLintersPresent.length > 0; - } - - public async enableLintingAsync(enable: boolean, resource?: Uri): Promise { - await this.configService.updateSetting('linting.enabled', enable, resource); - } - - public async getActiveLinters(resource?: Uri): Promise { - return this.linters.filter((x) => x.isEnabled(resource)); - } - - public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise { - // ensure we only allow valid linters to be set, otherwise leave things alone. - // filter out any invalid products: - const validProducts = products.filter((product) => { - const foundIndex = this.linters.findIndex((validLinter) => validLinter.product === product); - return foundIndex !== -1; - }); - - // if we have valid linter product(s), enable only those - if (validProducts.length > 0) { - const active = await this.getActiveLinters(resource); - for (const x of active) { - await x.enableAsync(false, resource); - } - if (products.length > 0) { - const toActivate = this.linters.filter((x) => products.findIndex((p) => x.product === p) >= 0); - for (const x of toActivate) { - await x.enableAsync(true, resource); - } - await this.enableLintingAsync(true, resource); - } - } - } - - public async createLinter(product: Product, serviceContainer: IServiceContainer, resource?: Uri): Promise { - if (!(await this.isLintingEnabled(resource))) { - return new DisabledLinter(this.configService); - } - const error = 'Linter manager: Unknown linter'; - switch (product) { - case Product.bandit: - return new Bandit(serviceContainer); - case Product.flake8: - return new Flake8(serviceContainer, getOrCreateFlake8Prompt(serviceContainer)); - case Product.pylint: - return new Pylint(serviceContainer, getOrCreatePylintPrompt(serviceContainer)); - case Product.mypy: - return new MyPy(serviceContainer); - case Product.prospector: - return new Prospector(serviceContainer); - case Product.pylama: - return new PyLama(serviceContainer); - case Product.pydocstyle: - return new PyDocStyle(serviceContainer); - case Product.pycodestyle: - return new Pycodestyle(serviceContainer); - default: - traceError(error); - break; - } - throw new Error(error); - } -} diff --git a/extensions/positron-python/src/client/linters/lintingEngine.ts b/extensions/positron-python/src/client/linters/lintingEngine.ts deleted file mode 100644 index 2a4bf4e1084..00000000000 --- a/extensions/positron-python/src/client/linters/lintingEngine.ts +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Minimatch } from 'minimatch'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService } from '../common/types'; -import { isNotebookCell, noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; -import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; - -const PYTHON: vscode.DocumentFilter = { language: 'python' }; - -const lintSeverityToVSSeverity = new Map(); -lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); -lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); -lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); -lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); - -@injectable() -export class LintingEngine implements ILintingEngine { - private workspace: IWorkspaceService; - - private documents: IDocumentManager; - - private configurationService: IConfigurationService; - - private linterManager: ILinterManager; - - private diagnosticCollection: vscode.DiagnosticCollection; - - private pendingLintings = new Map(); - - private fileSystem: IFileSystem; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.documents = serviceContainer.get(IDocumentManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.configurationService = serviceContainer.get(IConfigurationService); - this.linterManager = serviceContainer.get(ILinterManager); - this.fileSystem = serviceContainer.get(IFileSystem); - this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); - } - - public get diagnostics(): vscode.DiagnosticCollection { - return this.diagnosticCollection; - } - - public clearDiagnostics(document: vscode.TextDocument): void { - if (this.diagnosticCollection.has(document.uri)) { - this.diagnosticCollection.delete(document.uri); - } - } - - public async lintOpenPythonFiles(trigger: LinterTrigger = 'auto'): Promise { - this.diagnosticCollection.clear(); - const promises = this.documents.textDocuments.map(async (document) => this.lintDocument(document, trigger)); - await Promise.all(promises); - return this.diagnosticCollection; - } - - public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - if (isNotebookCell(document)) { - return; - } - this.diagnosticCollection.set(document.uri, []); - - // Check if we need to lint this document - if (!(await this.shouldLintDocument(document, trigger))) { - return; - } - - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.get(document.uri.fsPath)!.cancel(); - this.pendingLintings.delete(document.uri.fsPath); - } - - const cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.delete(document.uri.fsPath); - } - }); - - this.pendingLintings.set(document.uri.fsPath, cancelToken); - - const activeLinters = await this.linterManager.getActiveLinters(document.uri); - const promises: Promise[] = activeLinters.map(async (info: ILinterInfo) => { - const stopWatch = new StopWatch(); - const linter = await this.linterManager.createLinter(info.product, this.serviceContainer, document.uri); - const promise = linter.lint(document, cancelToken.token); - this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); - return promise; - }); - - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in. - let diagnostics: vscode.Diagnostic[] = []; - const settings = this.configurationService.getSettings(document.uri); - - for (const p of promises) { - const msgs = await p; - if (cancelToken.token.isCancellationRequested) { - break; - } - - if (this.isDocumentOpen(document.uri)) { - // Build the message and suffix the message with the name of the linter used. - for (const m of msgs) { - diagnostics.push(this.createDiagnostics(m, document)); - } - // Limit the number of messages to the max value. - diagnostics = diagnostics.filter((_value, index) => index <= settings.linting.maxNumberOfProblems); - } - } - // Set all diagnostics found in this pass, as this method always clears existing diagnostics. - this.diagnosticCollection.set(document.uri, diagnostics); - } - - // eslint-disable-next-line class-methods-use-this - private sendLinterRunTelemetry( - info: ILinterInfo, - resource: vscode.Uri, - promise: Promise, - stopWatch: StopWatch, - trigger: LinterTrigger, - ): void { - const linterExecutablePathName = info.pathName(resource); - const properties: LintingTelemetry = { - tool: info.id, - hasCustomArgs: info.linterArgs(resource).length > 0, - trigger, - executableSpecified: linterExecutablePathName !== info.id, - }; - sendTelemetryWhenDone(EventName.LINTING, promise, stopWatch, properties); - } - - private isDocumentOpen(uri: vscode.Uri): boolean { - return this.documents.textDocuments.some((document) => document.uri.fsPath === uri.fsPath); - } - - // eslint-disable-next-line class-methods-use-this - private createDiagnostics(message: ILintMessage, _document: vscode.TextDocument): vscode.Diagnostic { - const position = new vscode.Position(message.line - 1, message.column); - let endPosition: vscode.Position = position; - if (message.endLine && message.endColumn) { - endPosition = new vscode.Position(message.endLine - 1, message.endColumn); - } - const range = new vscode.Range(position, endPosition); - - const severity = lintSeverityToVSSeverity.get(message.severity!)!; - const diagnostic = new vscode.Diagnostic(range, message.message, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - return diagnostic; - } - - private async shouldLintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(document.uri); - if (!interpreter && trigger === 'manual') { - this.serviceContainer - .get(ICommandManager) - .executeCommand(Commands.TriggerEnvironmentSelection, document.uri) - .then(noop, noop); - return false; - } - if (!(await this.linterManager.isLintingEnabled(document.uri))) { - this.diagnosticCollection.set(document.uri, []); - return false; - } - - if (document.languageId !== PYTHON.language) { - return false; - } - - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = - workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; - const relativeFileName = - typeof workspaceRootPath === 'string' - ? path.relative(workspaceRootPath, document.fileName) - : document.fileName; - - const settings = this.configurationService.getSettings(document.uri); - // { dot: true } is important so dirs like `.venv` will be matched by globs - const ignoreMinmatches = settings.linting.ignorePatterns.map( - (pattern) => new Minimatch(pattern, { dot: true }), - ); - if (ignoreMinmatches.some((matcher) => matcher.match(document.fileName) || matcher.match(relativeFileName))) { - return false; - } - if (document.uri.scheme !== 'file' || !document.uri.fsPath) { - return false; - } - return this.fileSystem.fileExists(document.uri.fsPath); - } -} diff --git a/extensions/positron-python/src/client/linters/mypy.ts b/extensions/positron-python/src/client/linters/mypy.ts deleted file mode 100644 index f39eef99b42..00000000000 --- a/extensions/positron-python/src/client/linters/mypy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { escapeRegExp } from 'lodash'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -export function getRegex(filepath: string): string { - return `${escapeRegExp(filepath)}:(?\\d+)(:(?\\d+))?: (?\\w+): (?.*)\\r?(\\n|$)`; -} -const COLUMN_OFF_SET = 1; - -export class MyPy extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.mypy, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const relativeFilePath = document.uri.fsPath.slice(this.getWorkspaceRootPath(document).length + 1); - const regex = getRegex(relativeFilePath); - const messages = await this.run([document.uri.fsPath], document, cancellation, regex); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); - msg.code = msg.type; - }); - return messages; - } -} diff --git a/extensions/positron-python/src/client/linters/prompts/common.ts b/extensions/positron-python/src/client/linters/prompts/common.ts deleted file mode 100644 index ab88282db60..00000000000 --- a/extensions/positron-python/src/client/linters/prompts/common.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ShowToolsExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IExtensions, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; - -export function isExtensionDisabled(serviceContainer: IServiceContainer, extensionId: string): boolean { - const extensions: IExtensions = serviceContainer.get(IExtensions); - // When debugging the python extension this `extensionPath` below will point to your repo. - // If you are debugging this feature then set the `extensionPath` to right location after - // the next line. - const pythonExt = extensions.getExtension('ms-python.python'); - if (pythonExt) { - let found = false; - traceLog(`Extension search path: ${path.dirname(pythonExt.extensionPath)}`); - fs.readdirSync(path.dirname(pythonExt.extensionPath), { withFileTypes: false }).forEach((s) => { - if (s.toString().startsWith(extensionId)) { - found = true; - } - }); - return found; - } - return false; -} - -/** - * Detects if extension is installed and enabled. - */ -export function isExtensionEnabled(serviceContainer: IServiceContainer, extensionId: string): boolean { - const extensions: IExtensions = serviceContainer.get(IExtensions); - const extension = extensions.getExtension(extensionId); - return extension !== undefined; -} - -export function doNotShowPromptState( - serviceContainer: IServiceContainer, - promptKey: string, -): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export function inToolsExtensionsExperiment(serviceContainer: IServiceContainer): Promise { - const experiments: IExperimentService = serviceContainer.get(IExperimentService); - return experiments.inExperiment(ShowToolsExtensionPrompt.experiment); -} diff --git a/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts b/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts deleted file mode 100644 index fa1969df682..00000000000 --- a/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { doNotShowPromptState, inToolsExtensionsExperiment, isExtensionDisabled, isExtensionEnabled } from './common'; -import { IToolsExtensionPrompt } from './types'; - -export const FLAKE8_EXTENSION = 'ms-python.flake8'; -const FLAKE8_PROMPT_DONOTSHOW_KEY = 'showFlake8ExtensionPrompt'; - -export class Flake8ExtensionPrompt implements IToolsExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(this.serviceContainer, FLAKE8_EXTENSION); - if (isEnabled || isExtensionDisabled(this.serviceContainer, FLAKE8_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: FLAKE8_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, FLAKE8_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - if (!(await inToolsExtensionsExperiment(this.serviceContainer))) { - return false; - } - - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.flake8PromptMessage, - ToolsExtensions.installFlake8Extension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - doNotShow.updateValue(true); - return false; - } - - if (response === ToolsExtensions.installFlake8Extension) { - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - return false; - } -} - -let _prompt: IToolsExtensionPrompt | undefined; -export function getOrCreateFlake8Prompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { - if (!_prompt) { - _prompt = new Flake8ExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts b/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts deleted file mode 100644 index 37e58324307..00000000000 --- a/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { doNotShowPromptState, inToolsExtensionsExperiment, isExtensionDisabled, isExtensionEnabled } from './common'; -import { IToolsExtensionPrompt } from './types'; - -export const PYLINT_EXTENSION = 'ms-python.pylint'; -const PYLINT_PROMPT_DONOTSHOW_KEY = 'showPylintExtensionPrompt'; - -export class PylintExtensionPrompt implements IToolsExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(this.serviceContainer, PYLINT_EXTENSION); - if (isEnabled || isExtensionDisabled(this.serviceContainer, PYLINT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: PYLINT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, PYLINT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - if (!(await inToolsExtensionsExperiment(this.serviceContainer))) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: PYLINT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.pylintPromptMessage, - ToolsExtensions.installPylintExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: PYLINT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installPylintExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: PYLINT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: PYLINT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: IToolsExtensionPrompt | undefined; -export function getOrCreatePylintPrompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { - if (!_prompt) { - _prompt = new PylintExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/extensions/positron-python/src/client/linters/prompts/types.ts b/extensions/positron-python/src/client/linters/prompts/types.ts deleted file mode 100644 index d7c884b3a00..00000000000 --- a/extensions/positron-python/src/client/linters/prompts/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -export interface IToolsExtensionPrompt { - showPrompt(): Promise; -} diff --git a/extensions/positron-python/src/client/linters/prospector.ts b/extensions/positron-python/src/client/linters/prospector.ts deleted file mode 100644 index fa4b3907255..00000000000 --- a/extensions/positron-python/src/client/linters/prospector.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -interface IProspectorResponse { - messages: IProspectorMessage[]; -} -interface IProspectorMessage { - source: string; - message: string; - code: string; - location: IProspectorLocation; -} -interface IProspectorLocation { - function: string; - path: string; - line: number; - character: number; - module: 'beforeFormat'; -} - -export class Prospector extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.prospector, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const cwd = this.getWorkingDirectoryPath(document); - const relativePath = path.relative(cwd, document.uri.fsPath); - return this.run([relativePath], document, cancellation); - } - - protected async parseMessages( - output: string, - _document: TextDocument, - _token: CancellationToken, - _regEx: string, - ): Promise { - let parsedData: IProspectorResponse; - try { - parsedData = JSON.parse(output); - } catch (ex) { - traceLog(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}`); - traceLog(output); - traceError('Failed to parse Prospector output', ex); - return []; - } - return parsedData.messages - .filter((_value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems) - .map((msg) => { - const lineNumber = - msg.location.line === null || Number.isNaN(msg.location.line) ? 1 : msg.location.line; - - return { - code: msg.code, - message: msg.message, - column: msg.location.character, - line: lineNumber, - type: msg.code, - provider: `${this.info.id} - ${msg.source}`, - }; - }); - } -} diff --git a/extensions/positron-python/src/client/linters/pycodestyle.ts b/extensions/positron-python/src/client/linters/pycodestyle.ts deleted file mode 100644 index 30517980e83..00000000000 --- a/extensions/positron-python/src/client/linters/pycodestyle.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -const COLUMN_OFF_SET = 1; - -export class Pycodestyle extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pycodestyle, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity( - msg.type, - this.pythonSettings.linting.pycodestyleCategorySeverity, - ); - }); - return messages; - } -} diff --git a/extensions/positron-python/src/client/linters/pydocstyle.ts b/extensions/positron-python/src/client/linters/pydocstyle.ts deleted file mode 100644 index 4851190a92a..00000000000 --- a/extensions/positron-python/src/client/linters/pydocstyle.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; -import { isWindows } from '../common/platform/platformService'; - -export class PyDocStyle extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pydocstyle, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation); - // All messages in pep8 are treated as warnings for now. - messages.forEach((msg) => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } - - protected async parseMessages( - output: string, - document: TextDocument, - _token: CancellationToken, - _regEx: string, - ): Promise { - let outputLines = output.split(/\r?\n/g); - const baseFileName = path.basename(document.uri.fsPath); - - // Remember, the first line of the response contains the file name and line number, the next line contains the error message. - // So we have two lines per message, hence we need to take lines in pairs. - const maxLines = this.pythonSettings.linting.maxNumberOfProblems * 2; - // First line is almost always empty. - const oldOutputLines = outputLines.filter((line) => line.length > 0); - outputLines = []; - for (let counter = 0; counter < oldOutputLines.length / 2; counter += 1) { - outputLines.push(oldOutputLines[2 * counter] + oldOutputLines[2 * counter + 1]); - } - - return ( - outputLines - .filter((value, index) => index < maxLines && value.indexOf(':') >= 0) - .map((line) => { - // Windows will have a : after the drive letter (e.g. c:\). - if (isWindows()) { - return line.substring(line.indexOf(`${baseFileName}:`) + baseFileName.length + 1).trim(); - } - return line.substring(line.indexOf(':') + 1).trim(); - }) - // Iterate through the lines (skipping the messages). - // So, just iterate the response in pairs. - .map((line) => { - try { - if (line.trim().length === 0) { - return undefined; - } - const lineNumber = parseInt(line.substring(0, line.indexOf(' ')), 10); - const part = line.substring(line.indexOf(':') + 1).trim(); - const code = part.substring(0, part.indexOf(':')).trim(); - const message = part.substring(part.indexOf(':') + 1).trim(); - - const sourceLine = document.lineAt(lineNumber - 1).text; - const trimmedSourceLine = sourceLine.trim(); - const sourceStart = sourceLine.indexOf(trimmedSourceLine); - - return { - code, - message, - column: sourceStart, - line: lineNumber, - type: '', - provider: this.info.id, - } as ILintMessage; - } catch (ex) { - traceError(`Failed to parse pydocstyle line '${line}'`, ex); - } - - return undefined; - }) - .filter((item) => item !== undefined) - .map((item) => item!) - ); - } -} diff --git a/extensions/positron-python/src/client/linters/pylama.ts b/extensions/positron-python/src/client/linters/pylama.ts deleted file mode 100644 index d5930c83944..00000000000 --- a/extensions/positron-python/src/client/linters/pylama.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -/** - * Example messages to parse from PyLama - * 1. Linter: pycodestyle - recent version removed an extra colon (:) after line:col, hence made it optional in the regex (to be backward compatibile) - * `src/test_py.py:23:60 [E] E226 missing whitespace around arithmetic operator [pycodestyle]` - * 2. Linter: mypy - output is missing the error code, something like `E226` - hence made it optional in the regex - * `src/test_py.py:7:4 [E] Argument 1 to "fn" has incompatible type "str"; expected "int" [mypy]` - */ - -const REGEX = - '(?.py):(?\\d+):(?\\d+):? \\[(?\\w+)\\]( (?\\w\\d+)?:?)? (?.*)\\r?(\\n|$)'; -const COLUMN_OFF_SET = 1; - -export class PyLama extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pylama, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation, REGEX); - // All messages in pylama are treated as warnings for now. - messages.forEach((msg) => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } -} diff --git a/extensions/positron-python/src/client/linters/pylint.ts b/extensions/positron-python/src/client/linters/pylint.ts deleted file mode 100644 index 0b635417f90..00000000000 --- a/extensions/positron-python/src/client/linters/pylint.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { isExtensionEnabled } from './prompts/common'; -import { PYLINT_EXTENSION } from './prompts/pylintPrompt'; -import { IToolsExtensionPrompt } from './prompts/types'; -import { ILintMessage } from './types'; - -interface IJsonMessage { - column: number | null; - line: number; - message: string; - symbol: string; - type: string; - endLine?: number | null; - endColumn?: number | null; -} - -export class Pylint extends BaseLinter { - constructor(serviceContainer: IServiceContainer, private readonly prompt: IToolsExtensionPrompt) { - super(Product.pylint, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - await this.prompt.showPrompt(); - - if (isExtensionEnabled(this.serviceContainer, PYLINT_EXTENSION)) { - traceLog( - 'LINTING: Skipping linting from Python extension, since Pylint extension is installed and enabled.', - ); - return []; - } - - const { uri } = document; - const settings = this.configService.getSettings(uri); - const args = [uri.fsPath]; - const messages = await this.run(args, document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, settings.linting.pylintCategorySeverity); - }); - return messages; - } - - private parseOutputMessage(outputMsg: IJsonMessage, colOffset = 0): ILintMessage | undefined { - // Both 'endLine' and 'endColumn' are only present on pylint 2.12.2+ - // If present, both can still be 'null' if AST node didn't have endLine and / or endColumn information. - // If 'endColumn' is 'null' or not preset, set it to 'undefined' to - // prevent the lintingEngine from inferring an error range. - if (outputMsg.endColumn) { - outputMsg.endColumn = outputMsg.endColumn <= 0 ? 0 : outputMsg.endColumn - colOffset; - } else { - outputMsg.endColumn = undefined; - } - - return { - code: outputMsg.symbol, - message: outputMsg.message, - column: outputMsg.column === null || outputMsg.column <= 0 ? 0 : outputMsg.column - colOffset, - line: outputMsg.line, - type: outputMsg.type, - provider: this.info.id, - endLine: outputMsg.endLine === null ? undefined : outputMsg.endLine, - endColumn: outputMsg.endColumn, - }; - } - - protected async parseMessages( - output: string, - _document: TextDocument, - _token: CancellationToken, - _: string, - ): Promise { - const messages: ILintMessage[] = []; - try { - const parsedOutput: IJsonMessage[] = JSON.parse(output); - for (const outputMsg of parsedOutput) { - const msg = this.parseOutputMessage(outputMsg, this.columnOffset); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } - } catch (ex) { - traceError(`Linter '${this.info.id}' failed to parse the output '${output}.`, ex); - } - return messages; - } -} diff --git a/extensions/positron-python/src/client/linters/serviceRegistry.ts b/extensions/positron-python/src/client/linters/serviceRegistry.ts deleted file mode 100644 index 26ada4d0cc8..00000000000 --- a/extensions/positron-python/src/client/linters/serviceRegistry.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IExtensionActivationService } from '../activation/types'; -import { IServiceManager } from '../ioc/types'; -import { LinterProvider } from '../providers/linterProvider'; -import { LinterManager } from './linterManager'; -import { LintingEngine } from './lintingEngine'; -import { ILinterManager, ILintingEngine } from './types'; - -export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton(ILintingEngine, LintingEngine); - serviceManager.addSingleton(ILinterManager, LinterManager); - serviceManager.addSingleton(IExtensionActivationService, LinterProvider); -} diff --git a/extensions/positron-python/src/client/linters/types.ts b/extensions/positron-python/src/client/linters/types.ts deleted file mode 100644 index b24fe508ea1..00000000000 --- a/extensions/positron-python/src/client/linters/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import { ExecutionInfo, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { LinterTrigger } from '../telemetry/types'; - -export interface IErrorHandler { - handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise; -} - -export enum LinterId { - Flake8 = 'flake8', - MyPy = 'mypy', - PyCodeStyle = 'pycodestyle', - Prospector = 'prospector', - PyDocStyle = 'pydocstyle', - PyLama = 'pylama', - PyLint = 'pylint', - Bandit = 'bandit', -} - -export interface ILinterInfo { - readonly id: LinterId; - readonly product: Product; - readonly pathSettingName: string; - readonly argsSettingName: string; - readonly enabledSettingName: string; - readonly configFileNames: string[]; - enableAsync(enabled: boolean, resource?: vscode.Uri): Promise; - isEnabled(resource?: vscode.Uri): boolean; - pathName(resource?: vscode.Uri): string; - linterArgs(resource?: vscode.Uri): string[]; - getExecutionInfo(customArgs: string[], resource?: vscode.Uri): ExecutionInfo; -} - -export interface ILinter { - readonly info: ILinterInfo; - lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; -} - -export const ILinterManager = Symbol('ILinterManager'); -export interface ILinterManager { - getAllLinterInfos(): ILinterInfo[]; - getLinterInfo(product: Product): ILinterInfo; - getActiveLinters(resource?: vscode.Uri): Promise; - isLintingEnabled(resource?: vscode.Uri): Promise; - enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise; - setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise; - createLinter(product: Product, serviceContainer: IServiceContainer, resource?: vscode.Uri): Promise; -} - -export interface ILintMessage { - line: number; - column: number; - endLine?: number; - endColumn?: number; - code: string | undefined; - message: string; - type: string; - severity?: LintMessageSeverity; - provider: string; -} -export enum LintMessageSeverity { - Hint, - Error, - Warning, - Information, -} - -export const ILintingEngine = Symbol('ILintingEngine'); -export interface ILintingEngine { - readonly diagnostics: vscode.DiagnosticCollection; - lintOpenPythonFiles(trigger?: LinterTrigger): Promise; - lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise; - clearDiagnostics(document: vscode.TextDocument): void; -} diff --git a/extensions/positron-python/src/client/logging/settingLogs.ts b/extensions/positron-python/src/client/logging/settingLogs.ts new file mode 100644 index 00000000000..257e204e151 --- /dev/null +++ b/extensions/positron-python/src/client/logging/settingLogs.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { l10n } from 'vscode'; +import { traceError, traceInfo } from '.'; +import { Commands, PVSC_EXTENSION_ID } from '../common/constants'; +import { showWarningMessage } from '../common/vscodeApis/windowApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { Common } from '../common/utils/localize'; +import { executeCommand } from '../common/vscodeApis/commandApis'; + +function logOnLegacyFormatterSetting(): boolean { + let usesLegacyFormatter = false; + getWorkspaceFolders()?.forEach(async (workspace) => { + let config = getConfiguration('editor', { uri: workspace.uri, languageId: 'python' }); + if (!config) { + config = getConfiguration('editor', workspace.uri); + if (!config) { + traceError('Unable to get editor configuration'); + } + } + const formatter = config.get('defaultFormatter', ''); + traceInfo(`Default formatter is set to ${formatter} for workspace ${workspace.uri.fsPath}`); + if (formatter === PVSC_EXTENSION_ID) { + usesLegacyFormatter = true; + traceError( + 'The setting "editor.defaultFormatter" for Python is set to "ms-python.python" which is deprecated.', + ); + traceError('Formatting features have been moved to separate formatter extensions.'); + traceError('See here for more information: https://code.visualstudio.com/docs/python/formatting'); + traceError('Please install the formatter extension you prefer and set it as the default formatter.'); + traceError('For `autopep8` use: https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8'); + traceError( + 'For `black` use: https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter', + ); + traceError('For `yapf` use: https://marketplace.visualstudio.com/items?itemName=eeyore.yapf'); + } + }); + return usesLegacyFormatter; +} + +function logOnLegacyLinterSetting(): boolean { + let usesLegacyLinter = false; + getWorkspaceFolders()?.forEach(async (workspace) => { + let config = getConfiguration('python', { uri: workspace.uri, languageId: 'python' }); + if (!config) { + config = getConfiguration('python', workspace.uri); + if (!config) { + traceError('Unable to get editor configuration'); + } + } + + const linters: string[] = [ + 'pylint', + 'flake8', + 'mypy', + 'pydocstyle', + 'pylama', + 'pycodestyle', + 'bandit', + 'prospector', + ]; + + linters.forEach((linter) => { + const linterEnabled = config.get(`linting.${linter}Enabled`, false); + if (linterEnabled) { + usesLegacyLinter = true; + traceError(`Following setting is deprecated: "python.linting.${linter}Enabled"`); + traceError( + `All settings starting with "python.linting." are deprecated and can be removed from settings.`, + ); + traceError('Linting features have been moved to separate linter extensions.'); + traceError('See here for more information: https://code.visualstudio.com/docs/python/linting'); + if (linter === 'pylint' || linter === 'flake8') { + traceError( + `Please install "${linter}" extension: https://marketplace.visualstudio.com/items?itemName=ms-python.${linter}`, + ); + } else if (linter === 'mypy') { + traceError( + `Please install "${linter}" extension: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker`, + ); + } else if (['pydocstyle', 'pylama', 'pycodestyle', 'bandit'].includes(linter)) { + traceError( + `Selected linter "${linter}" may be supported by extensions like "Ruff", which include several linter rules: https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff`, + ); + } + } + }); + }); + + return usesLegacyLinter; +} + +let _isShown = false; +async function notifyLegacySettings(): Promise { + if (_isShown) { + return; + } + _isShown = true; + const response = await showWarningMessage( + l10n.t( + `You have deprecated linting or formatting settings for Python. Please see the [logs](command:${Commands.ViewOutput}) for more details.`, + ), + Common.learnMore, + ); + if (response === Common.learnMore) { + executeCommand('vscode.open', 'https://aka.ms/AAlgvkb'); + } +} + +export function logAndNotifyOnLegacySettings(): void { + const usesLegacyFormatter = logOnLegacyFormatterSetting(); + const usesLegacyLinter = logOnLegacyLinterSetting(); + + if (usesLegacyFormatter || usesLegacyLinter) { + setImmediate(() => notifyLegacySettings().ignoreErrors()); + } +} diff --git a/extensions/positron-python/src/client/providers/codeActionProvider/isortPrompt.ts b/extensions/positron-python/src/client/providers/codeActionProvider/isortPrompt.ts deleted file mode 100644 index ffef481b498..00000000000 --- a/extensions/positron-python/src/client/providers/codeActionProvider/isortPrompt.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionDisabled, isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const ISORT_EXTENSION = 'ms-python.isort'; -const ISORT_PROMPT_DONOTSHOW_KEY = 'showISortExtensionPrompt'; - -function doNotShowPromptState(serviceContainer: IServiceContainer, promptKey: string): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export class ISortExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(ISORT_EXTENSION); - if (isEnabled || isExtensionDisabled(ISORT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: ISORT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, ISORT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: ISORT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.isortPromptMessage, - ToolsExtensions.installISortExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installISortExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: ISORT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', ISORT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: ISortExtensionPrompt | undefined; -export function getOrCreateISortPrompt(serviceContainer: IServiceContainer): ISortExtensionPrompt { - if (!_prompt) { - _prompt = new ISortExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/extensions/positron-python/src/client/providers/codeActionProvider/main.ts b/extensions/positron-python/src/client/providers/codeActionProvider/main.ts index 40afd4dbb2b..259f4284860 100644 --- a/extensions/positron-python/src/client/providers/codeActionProvider/main.ts +++ b/extensions/positron-python/src/client/providers/codeActionProvider/main.ts @@ -4,23 +4,14 @@ import { inject, injectable } from 'inversify'; import * as vscodeTypes from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { Commands } from '../../common/constants'; import { IDisposableRegistry } from '../../common/types'; -import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; -import { getOrCreateISortPrompt, ISORT_EXTENSION } from './isortPrompt'; import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; @injectable() export class CodeActionProviderService implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - ) {} + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} public async activate(): Promise { // eslint-disable-next-line global-require @@ -35,19 +26,5 @@ export class CodeActionProviderService implements IExtensionSingleActivationServ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], }), ); - this.disposableRegistry.push( - registerCommand(Commands.Sort_Imports, async () => { - const prompt = getOrCreateISortPrompt(this.serviceContainer); - await prompt.showPrompt(); - if (!isExtensionEnabled(ISORT_EXTENSION)) { - traceLog( - 'Sort Imports: Please install and enable `ms-python.isort` extension to use this feature.', - ); - return; - } - - executeCommand('editor.action.organizeImports'); - }), - ); } } diff --git a/extensions/positron-python/src/client/providers/formatProvider.ts b/extensions/positron-python/src/client/providers/formatProvider.ts deleted file mode 100644 index 1ea239c03be..00000000000 --- a/extensions/positron-python/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_LANGUAGE } from '../common/constants'; -import { IConfigurationService } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { AutoPep8Formatter } from '../formatters/autoPep8Formatter'; -import { BaseFormatter } from '../formatters/baseFormatter'; -import { BlackFormatter } from '../formatters/blackFormatter'; -import { DummyFormatter } from '../formatters/dummyFormatter'; -import { YapfFormatter } from '../formatters/yapfFormatter'; - -export class PythonFormattingEditProvider - implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { - private readonly config: IConfigurationService; - - private readonly workspace: IWorkspaceService; - - private readonly documentManager: IDocumentManager; - - private readonly commands: ICommandManager; - - private formatters = new Map(); - - private disposables: vscode.Disposable[] = []; - - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - private documentVersionBeforeFormatting = -1; - - private formatterMadeChanges = false; - - private saving = false; - - public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { - const yapfFormatter = new YapfFormatter(serviceContainer); - const autoPep8 = new AutoPep8Formatter(serviceContainer); - const black = new BlackFormatter(serviceContainer); - const dummy = new DummyFormatter(serviceContainer); - this.formatters.set(yapfFormatter.Id, yapfFormatter); - this.formatters.set(black.Id, black); - this.formatters.set(autoPep8.Id, autoPep8); - this.formatters.set(dummy.Id, dummy); - - this.commands = serviceContainer.get(ICommandManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); - this.config = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - this.disposables.push( - this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)), - ); - this.disposables.push( - interpreterService.onDidChangeInterpreter(async () => { - if (this.documentManager.activeTextEditor) { - return this.onSaveDocument(this.documentManager.activeTextEditor.document); - } - - return undefined; - }), - ); - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - public provideDocumentFormattingEdits( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits( - document: vscode.TextDocument, - range: vscode.Range | undefined, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. - // Workaround is to resolve promise to nothing here, then execute format document and force new save. - // However, we need to know if this is 'format document' or formatting on save. - - if (this.saving || document.languageId !== PYTHON_LANGUAGE) { - // We are saving after formatting (see onSaveDocument below) - // so we do not want to format again. - return []; - } - - // Remember content before formatting so we can detect if - // formatting edits have been really applied - const editorConfig = this.workspace.getConfiguration('editor', document.uri); - if (editorConfig.get('formatOnSave') === true) { - this.documentVersionBeforeFormatting = document.version; - } - - const settings = this.config.getSettings(document.uri); - const formatter = this.formatters.get(settings.formatting.provider)!; - const edits = await formatter.formatDocument(document, options, token, range); - - this.formatterMadeChanges = edits.length > 0; - return edits; - } - - private async onSaveDocument(document: vscode.TextDocument): Promise { - // Promise was rejected = formatting took too long. - // Don't format inside the event handler, do it on timeout - setTimeout(() => { - try { - if ( - this.formatterMadeChanges && - !document.isDirty && - document.version === this.documentVersionBeforeFormatting - ) { - // Formatter changes were not actually applied due to the timeout on save. - // Force formatting now and then save the document. - this.commands.executeCommand('editor.action.formatDocument').then(async () => { - this.saving = true; - await document.save(); - this.saving = false; - }); - } - } finally { - this.documentVersionBeforeFormatting = -1; - this.saving = false; - this.formatterMadeChanges = false; - } - }, 50); - } -} diff --git a/extensions/positron-python/src/client/providers/linterProvider.ts b/extensions/positron-python/src/client/providers/linterProvider.ts deleted file mode 100644 index 7821eaeccd5..00000000000 --- a/extensions/positron-python/src/client/providers/linterProvider.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ConfigurationChangeEvent, Disposable, TextDocument, Uri, workspace } from 'vscode'; -import { IExtensionActivationService } from '../activation/types'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IDisposable } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { ILinterManager, ILintingEngine } from '../linters/types'; - -@injectable() -export class LinterProvider implements IExtensionActivationService, Disposable { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private interpreterService: IInterpreterService; - - private documents: IDocumentManager; - - private configuration: IConfigurationService; - - private linterManager: ILinterManager; - - private engine: ILintingEngine; - - private fs: IFileSystem; - - private readonly disposables: IDisposable[] = []; - - private workspaceService: IWorkspaceService; - - private activatedOnce = false; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.serviceContainer = serviceContainer; - this.fs = this.serviceContainer.get(IFileSystem); - this.engine = this.serviceContainer.get(ILintingEngine); - this.linterManager = this.serviceContainer.get(ILinterManager); - this.interpreterService = this.serviceContainer.get(IInterpreterService); - this.documents = this.serviceContainer.get(IDocumentManager); - this.configuration = this.serviceContainer.get(IConfigurationService); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - } - - public async activate(): Promise { - if (this.activatedOnce) { - return; - } - this.activatedOnce = true; - this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles())); - - this.documents.onDidOpenTextDocument((e) => this.onDocumentOpened(e), this.disposables); - this.documents.onDidCloseTextDocument((e) => this.onDocumentClosed(e), this.disposables); - this.documents.onDidSaveTextDocument((e) => this.onDocumentSaved(e), this.disposables); - - const disposable = this.workspaceService.onDidChangeConfiguration(this.lintSettingsChangedHandler.bind(this)); - this.disposables.push(disposable); - - // On workspace reopen we don't get `onDocumentOpened` since it is first opened - // and then the extension is activated. So schedule linting pass now. - if (!isTestExecution()) { - const timer = setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200); - this.disposables.push({ dispose: () => clearTimeout(timer) }); - } - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - private isDocumentOpen(uri: Uri): boolean { - return this.documents.textDocuments.some((document) => this.fs.arePathsSame(document.uri.fsPath, uri.fsPath)); - } - - private lintSettingsChangedHandler(e: ConfigurationChangeEvent) { - // Look for python files that belong to the specified workspace folder. - workspace.textDocuments.forEach((document) => { - if (e.affectsConfiguration('python.linting', document.uri)) { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - }); - } - - private onDocumentOpened(document: TextDocument): void { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - - private onDocumentSaved(document: TextDocument): void { - const settings = this.configuration.getSettings(document.uri); - if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) { - this.engine.lintDocument(document, 'save').ignoreErrors(); - return; - } - - this.linterManager - .getActiveLinters(document.uri) - .then((linters) => { - const fileName = path.basename(document.uri.fsPath).toLowerCase(); - const watchers = linters.filter((info) => info.configFileNames.indexOf(fileName) >= 0); - if (watchers.length > 0) { - setTimeout(() => this.engine.lintOpenPythonFiles(), 1000); - } - }) - .ignoreErrors(); - } - - private onDocumentClosed(document: TextDocument) { - if (!document || !document.fileName || !document.uri) { - return; - } - // Check if this document is still open as a duplicate editor. - if (!this.isDocumentOpen(document.uri)) { - this.engine.clearDiagnostics(document); - } - } -} diff --git a/extensions/positron-python/src/client/providers/prompts/installFormatterPrompt.ts b/extensions/positron-python/src/client/providers/prompts/installFormatterPrompt.ts deleted file mode 100644 index 5743f840205..00000000000 --- a/extensions/positron-python/src/client/providers/prompts/installFormatterPrompt.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IDisposableRegistry } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { getConfiguration, onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; -import { - doNotShowPromptState, - inFormatterExtensionExperiment, - installFormatterExtension, - updateDefaultFormatter, -} from './promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from './types'; - -const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInstallPrompt'; - -@injectable() -export class InstallFormatterPrompt implements IInstallFormatterPrompt { - private currentlyShown = false; - - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - - /* - * This method is called when the user saves a python file or a cell. - * Returns true if an extension was selected. Otherwise returns false. - */ - public async showInstallFormatterPrompt(resource?: Uri): Promise { - if (!inFormatterExtensionExperiment(this.serviceContainer)) { - return false; - } - - const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); - if (this.currentlyShown || promptState.value) { - return false; - } - - const config = getConfiguration('python', resource); - const formatter = config.get('formatting.provider', 'none'); - if (!['autopep8', 'black'].includes(formatter)) { - return false; - } - - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - const defaultFormatter = editorConfig.get('defaultFormatter', ''); - if ([BLACK_EXTENSION, AUTOPEP8_EXTENSION].includes(defaultFormatter)) { - return false; - } - - const black = isExtensionEnabled(BLACK_EXTENSION); - const autopep8 = isExtensionEnabled(AUTOPEP8_EXTENSION); - - let selection: string | undefined; - - if (black || autopep8) { - this.currentlyShown = true; - if (black && autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (black) { - selection = await showInformationMessage( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Black'; - } - } else if (autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Autopep8'; - } - } - } else if (formatter === 'black' && !black) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (formatter === 'autopep8' && !autopep8) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } - - let userSelectedAnExtension = false; - if (selection === 'Black') { - if (black) { - userSelectedAnExtension = true; - await updateDefaultFormatter(BLACK_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(BLACK_EXTENSION, resource); - } - } else if (selection === 'Autopep8') { - if (autopep8) { - userSelectedAnExtension = true; - await updateDefaultFormatter(AUTOPEP8_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(AUTOPEP8_EXTENSION, resource); - } - } else if (selection === Common.doNotShowAgain) { - userSelectedAnExtension = false; - await promptState.updateValue(true); - } else { - userSelectedAnExtension = false; - } - - this.currentlyShown = false; - return userSelectedAnExtension; - } -} - -export function registerInstallFormatterPrompt(serviceContainer: IServiceContainer): void { - const disposables = serviceContainer.get(IDisposableRegistry); - const installFormatterPrompt = serviceContainer.get(IInstallFormatterPrompt); - disposables.push( - onDidSaveTextDocument(async (e) => { - const editorConfig = getConfiguration('editor', { uri: e.uri, languageId: 'python' }); - if (e.languageId === 'python' && editorConfig.get('formatOnSave')) { - await installFormatterPrompt.showInstallFormatterPrompt(e.uri); - } - }), - ); -} diff --git a/extensions/positron-python/src/client/providers/prompts/promptUtils.ts b/extensions/positron-python/src/client/providers/prompts/promptUtils.ts deleted file mode 100644 index 05b1b28f061..00000000000 --- a/extensions/positron-python/src/client/providers/prompts/promptUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { ConfigurationTarget, Uri } from 'vscode'; -import { ShowFormatterExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isInsider } from '../../common/vscodeApis/extensionsApi'; -import { getConfiguration, getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; - -export function inFormatterExtensionExperiment(serviceContainer: IServiceContainer): boolean { - const experiment = serviceContainer.get(IExperimentService); - return experiment.inExperimentSync(ShowFormatterExtensionPrompt.experiment); -} - -export function doNotShowPromptState(key: string, serviceContainer: IServiceContainer): IPersistentState { - const persistFactory = serviceContainer.get(IPersistentStateFactory); - const promptState = persistFactory.createWorkspacePersistentState(key, false); - return promptState; -} - -export async function updateDefaultFormatter(extensionId: string, resource?: Uri): Promise { - const scope = getWorkspaceFolder(resource) ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; - - const config = getConfiguration('python', resource); - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - await editorConfig.update('defaultFormatter', extensionId, scope, true); - await config.update('formatting.provider', 'none', scope); -} - -export async function installFormatterExtension(extensionId: string, resource?: Uri): Promise { - await executeCommand('workbench.extensions.installExtension', extensionId, { - installPreReleaseVersion: isInsider(), - }); - - await updateDefaultFormatter(extensionId, resource); -} diff --git a/extensions/positron-python/src/client/providers/prompts/types.ts b/extensions/positron-python/src/client/providers/prompts/types.ts deleted file mode 100644 index 4edaadb46b4..00000000000 --- a/extensions/positron-python/src/client/providers/prompts/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; - -export const BLACK_EXTENSION = 'ms-python.black-formatter'; -export const AUTOPEP8_EXTENSION = 'ms-python.autopep8'; - -export const IInstallFormatterPrompt = Symbol('IInstallFormatterPrompt'); -export interface IInstallFormatterPrompt { - showInstallFormatterPrompt(resource?: Uri): Promise; -} diff --git a/extensions/positron-python/src/client/providers/serviceRegistry.ts b/extensions/positron-python/src/client/providers/serviceRegistry.ts index 70fc6dc3413..a96ec14ff5e 100644 --- a/extensions/positron-python/src/client/providers/serviceRegistry.ts +++ b/extensions/positron-python/src/client/providers/serviceRegistry.ts @@ -6,13 +6,10 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { CodeActionProviderService } from './codeActionProvider/main'; -import { InstallFormatterPrompt } from './prompts/installFormatterPrompt'; -import { IInstallFormatterPrompt } from './prompts/types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, CodeActionProviderService, ); - serviceManager.addSingleton(IInstallFormatterPrompt, InstallFormatterPrompt); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts index 57ae9187cdc..e4daeee640c 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts @@ -92,8 +92,8 @@ export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { } protected async initResources(): Promise { - this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.emitter.fire({}))); - this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.emitter.fire({}))); + this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.fire())); + this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.fire())); } // eslint-disable-next-line class-methods-use-this diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts new file mode 100644 index 00000000000..8a2b857d496 --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { getPythonSetting, onDidChangePythonSetting } from '../../../common/externalDependencies'; +import '../../../../common/extensions'; +import { traceVerbose } from '../../../../logging'; +import { DEFAULT_INTERPRETER_SETTING } from '../../../../common/constants'; + +export const DEFAULT_INTERPRETER_PATH_SETTING_KEY = 'defaultInterpreterPath'; + +/** + * Finds and resolves custom virtual environments that users have provided. + */ +export class CustomWorkspaceLocator extends FSWatchingLocator { + public readonly providerId: string = 'custom-workspace-locator'; + + constructor(private readonly root: string) { + super( + () => [], + async () => PythonEnvKind.Unknown, + ); + } + + protected async initResources(): Promise { + this.disposables.push( + onDidChangePythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, () => this.fire(), this.root), + ); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator { + const iterator = async function* (root: string) { + traceVerbose('Searching for custom workspace envs'); + const filename = getPythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, root); + if (!filename || filename === DEFAULT_INTERPRETER_SETTING) { + // If the user has not set a custom interpreter, our job is done. + return; + } + yield { kind: PythonEnvKind.Unknown, executablePath: filename }; + traceVerbose(`Finished searching for custom workspace envs`); + }; + return iterator(this.root); + } +} diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts index 0eb1d125200..7565913f0a7 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -128,6 +128,10 @@ export abstract class FSWatchingLocator extends LazyResourceBasedLocator { watchableRoots.forEach((root) => this.startWatchers(root)); } + protected fire(args = {}): void { + this.emitter.fire({ ...args, providerId: this.providerId }); + } + private startWatchers(root: string): void { const opts = this.creationOptions; if (isWatchingAFile(opts)) { diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts b/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts index ecb6f2212ab..b64d47f4226 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts @@ -175,8 +175,9 @@ export async function* getSubDirs( * Returns the value for setting `python.`. * @param name The name of the setting. */ -export function getPythonSetting(name: string): T | undefined { - const settings = internalServiceContainer.get(IConfigurationService).getSettings(); +export function getPythonSetting(name: string, root?: string): T | undefined { + const resource = root ? vscode.Uri.file(root) : undefined; + const settings = internalServiceContainer.get(IConfigurationService).getSettings(resource); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (settings as any)[name]; } @@ -186,9 +187,10 @@ export function getPythonSetting(name: string): T | undefined { * @param name The name of the setting. * @param callback The listener function to be called when the setting changes. */ -export function onDidChangePythonSetting(name: string, callback: () => void): IDisposable { +export function onDidChangePythonSetting(name: string, callback: () => void, root?: string): IDisposable { return vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { - if (event.affectsConfiguration(`python.${name}`)) { + const scope = root ? vscode.Uri.file(root) : undefined; + if (event.affectsConfiguration(`python.${name}`, scope)) { callback(); } }); diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 9dff50c5586..d923179fac7 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -106,7 +106,7 @@ async function createCondaEnv( out.subscribe( (value) => { const output = splitLines(value.out).join('\r\n'); - traceLog(output); + traceLog(output.trimEnd()); if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { condaEnvPath = getCondaEnvFromOutput(output); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 61850e404c3..7f854690f46 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -123,7 +123,7 @@ async function createVenv( out.subscribe( (value) => { const output = value.out.split(/\r?\n/g).join(os.EOL); - traceLog(output); + traceLog(output.trimEnd()); if (output.includes(VENV_CREATED_MARKER) || output.includes(VENV_EXISTING_MARKER)) { venvPath = getVenvFromOutput(output); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/index.ts b/extensions/positron-python/src/client/pythonEnvironments/index.ts index 8065811a8a6..5a5fceffa69 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/index.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/index.ts @@ -37,6 +37,7 @@ import { EnvsCollectionService } from './base/locators/composite/envsCollectionS import { IDisposable } from '../common/types'; import { traceError } from '../logging'; import { ActiveStateLocator } from './base/locators/lowLevel/activeStateLocator'; +import { CustomWorkspaceLocator } from './base/locators/lowLevel/customWorkspaceLocator'; /** * Set up the Python environments component (during extension activation).' @@ -182,7 +183,11 @@ function watchRoots(args: WatchRootsArgs): IDisposable { function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { const locators = new WorkspaceLocators(watchRoots, [ - (root: vscode.Uri) => [new WorkspaceVirtualEnvironmentLocator(root.fsPath), new PoetryLocator(root.fsPath)], + (root: vscode.Uri) => [ + new WorkspaceVirtualEnvironmentLocator(root.fsPath), + new PoetryLocator(root.fsPath), + new CustomWorkspaceLocator(root.fsPath), + ], // Add an ILocator factory func here for each kind of workspace-rooted locator. ]); ext.disposables.push(locators); diff --git a/extensions/positron-python/src/client/telemetry/constants.ts b/extensions/positron-python/src/client/telemetry/constants.ts index c680b91094c..9e29ef808d0 100644 --- a/extensions/positron-python/src/client/telemetry/constants.ts +++ b/extensions/positron-python/src/client/telemetry/constants.ts @@ -4,11 +4,8 @@ 'use strict'; export enum EventName { - FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS', - FORMAT = 'FORMAT.FORMAT', FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', - LINTING = 'LINTING', REPL = 'REPL', CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND', SELECT_INTERPRETER = 'SELECT_INTERPRETER', @@ -30,6 +27,7 @@ export enum EventName { PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', + TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT', REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', @@ -80,10 +78,8 @@ export enum EventName { DIAGNOSTICS_ACTION = 'DIAGNOSTICS.ACTION', DIAGNOSTICS_MESSAGE = 'DIAGNOSTICS.MESSAGE', - SELECT_LINTER = 'LINTING.SELECT', USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', - LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT', HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME', JEDI_LANGUAGE_SERVER_ENABLED = 'JEDI_LANGUAGE_SERVER.ENABLED', @@ -117,11 +113,6 @@ export enum EventName { ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', - - TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', - TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', - TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', - TOOLS_EXTENSIONS_PROMPT_DISMISSED = 'TOOLS_EXTENSIONS.PROMPT_DISMISSED', } export enum PlatformErrors { diff --git a/extensions/positron-python/src/client/telemetry/index.ts b/extensions/positron-python/src/client/telemetry/index.ts index 600f9a2d48f..f9ed98eb376 100644 --- a/extensions/positron-python/src/client/telemetry/index.ts +++ b/extensions/positron-python/src/client/telemetry/index.ts @@ -13,7 +13,6 @@ import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; import { DebugConfigurationType } from '../debugger/extension/types'; import { ConsoleType, TriggerType } from '../debugger/types'; -import { LinterId } from '../linters/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; import { TensorBoardPromptSelection, @@ -22,7 +21,7 @@ import { TensorBoardEntrypoint, } from '../tensorBoard/constants'; import { EventName } from './constants'; -import type { LinterTrigger, TestTool } from './types'; +import type { TestTool } from './types'; /** * Checks whether telemetry is supported. @@ -821,11 +820,11 @@ export interface IEventNamePropertyMapping { */ [EventName.EXECUTION_CODE]: { /** - * Whether the user executed a file in the terminal or just the selected text. + * Whether the user executed a file in the terminal or just the selected text or line by shift+enter. * * @type {('file' | 'selection')} */ - scope: 'file' | 'selection'; + scope: 'file' | 'selection' | 'line'; /** * How was the code executed (through the command or by clicking the `Run File` icon). * @@ -859,33 +858,7 @@ export interface IEventNamePropertyMapping { */ scope: 'file' | 'selection'; }; - /** - * Telemetry event sent with details when formatting a document - */ - /* __GDPR__ - "format.format" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "hascustomargs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "formatselection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.FORMAT]: { - /** - * Tool being used to format - */ - tool: 'autopep8' | 'black' | 'yapf'; - /** - * If arguments for formatter is provided in resource settings - */ - hasCustomArgs: boolean; - /** - * Carries `true` when formatting a selection of text, `false` otherwise - */ - formatSelection: boolean; - }; + /** * Telemetry event sent with the value of setting 'Format on type' */ @@ -902,16 +875,6 @@ export interface IEventNamePropertyMapping { */ enabled: boolean; }; - /** - * Telemetry event sent when sorting imports using formatter - */ - /* __GDPR__ - "format.sort_imports" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "originaleventname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.FORMAT_SORT_IMPORTS]: never | undefined; /** * Telemetry event sent with details when tracking imports @@ -921,7 +884,6 @@ export interface IEventNamePropertyMapping { "hashedname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } } */ - [EventName.HASHED_PACKAGE_NAME]: { /** * Hash of the package name @@ -931,33 +893,6 @@ export interface IEventNamePropertyMapping { hashedName: string; }; - /** - * Telemetry event sent with details of selection in prompt - * `Prompt message` :- 'Linter ${productName} is not installed' - */ - /* __GDPR__ - "linter_not_installed_prompt" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "action": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.LINTER_NOT_INSTALLED_PROMPT]: { - /** - * Name of the linter - * - * @type {LinterId} - */ - tool?: LinterId; - /** - * `select` When 'Select linter' option is selected - * `disablePrompt` When "Don't show again" option is selected - * `install` When 'Install' option is selected - * - * @type {('select' | 'disablePrompt' | 'install')} - */ - action: 'select' | 'disablePrompt' | 'install'; - }; - /** * Telemetry event sent when installing modules */ @@ -998,44 +933,6 @@ export interface IEventNamePropertyMapping { */ version?: string; }; - /** - * Telemetry sent with details immediately after linting a document completes - */ - /* __GDPR__ - "linting" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "hascustomargs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "executablespecified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.LINTING]: { - /** - * Name of the linter being used - * - * @type {LinterId} - */ - tool: LinterId; - /** - * If custom arguments for linter is provided in settings.json - * - * @type {boolean} - */ - hasCustomArgs: boolean; - /** - * Carries the source which triggered configuration of tests - * - * @type {LinterTrigger} - */ - trigger: LinterTrigger; - /** - * Carries `true` if linter executable is specified, `false` otherwise - * - * @type {boolean} - */ - executableSpecified: boolean; - }; /** * Telemetry event sent when an environment without contain a python binary is selected. */ @@ -1329,13 +1226,30 @@ export interface IEventNamePropertyMapping { selection: 'Allow' | 'Close' | undefined; }; /** - * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. + * Telemetry event sent with details when user clicks the prompt with the following message: + * + * 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?' */ /* __GDPR__ "conda_inherit_env_prompt" : { "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } } */ + [EventName.TERMINAL_DEACTIVATE_PROMPT]: { + /** + * `Yes` When 'Allow' option is selected + * `Close` When 'Close' option is selected + */ + selection: 'Edit script' | "Don't show again" | undefined; + }; + /** + * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. + */ + /* __GDPR__ + "require_jupyter_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } + } + */ [EventName.REQUIRE_JUPYTER_PROMPT]: { /** * `Yes` When 'Yes' option is selected @@ -1442,14 +1356,14 @@ export interface IEventNamePropertyMapping { [EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS]: { /** * List of valid experiments in the python.experiments.optInto setting - * @type {string[]} + * @type {string} */ - optedInto: string[]; + optedInto: string; /** * List of valid experiments in the python.experiments.optOutFrom setting - * @type {string[]} + * @type {string} */ - optedOutFrom: string[]; + optedOutFrom: string; }; /** * Telemetry event sent when LS is started for workspace (workspace folder in case of multi-root) @@ -1582,25 +1496,6 @@ export interface IEventNamePropertyMapping { } */ [EventName.REPL]: never | undefined; - /** - * Telemetry event sent with details of linter selected in quickpick of linter list. - */ - /* __GDPR__ - "linting.select" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.SELECT_LINTER]: { - /** - * The name of the linter - */ - tool?: LinterId; - /** - * Carries `true` if linter is enabled, `false` otherwise - */ - enabled: boolean; - }; /** * 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.) */ @@ -2172,53 +2067,6 @@ export interface IEventNamePropertyMapping { [EventName.ENVIRONMENT_CHECK_RESULT]: { result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; }; - /** - * Telemetry event sent when a linter or formatter extension is already installed. - */ - /* __GDPR__ - "tools_extensions.already_installed" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; - isEnabled: boolean; - }; - /** - * Telemetry event sent when install linter or formatter extension prompt is shown. - */ - /* __GDPR__ - "tools_extensions.prompt_shown" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; - }; - /** - * Telemetry event sent when clicking to install linter or formatter extension from the suggestion prompt. - */ - /* __GDPR__ - "tools_extensions.install_selected" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; - }; - /** - * Telemetry event sent when dismissing prompt suggesting to install the linter or formatter extension. - */ - /* __GDPR__ - "tools_extensions.prompt_dismissed" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "dismissType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; - dismissType: 'close' | 'doNotShow'; - }; /* __GDPR__ "query-expfeature" : { "owner": "luabud", diff --git a/extensions/positron-python/src/client/telemetry/types.ts b/extensions/positron-python/src/client/telemetry/types.ts index ae98707d94a..865dca278bf 100644 --- a/extensions/positron-python/src/client/telemetry/types.ts +++ b/extensions/positron-python/src/client/telemetry/types.ts @@ -8,10 +8,6 @@ import { EventName } from './constants'; export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; -export type LinterTrigger = 'auto' | 'save' | 'manual'; - -export type LintingTelemetry = IEventNamePropertyMapping[EventName.LINTING]; - export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYTHON_INTERPRETER]; export type DebuggerTelemetry = IEventNamePropertyMapping[EventName.DEBUGGER]; export type TestTool = 'pytest' | 'unittest'; diff --git a/extensions/positron-python/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/extensions/positron-python/src/client/tensorBoard/nbextensionCodeLensProvider.ts index 6d4c844cd39..afaaf116851 100644 --- a/extensions/positron-python/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ b/extensions/positron-python/src/client/tensorBoard/nbextensionCodeLensProvider.ts @@ -3,20 +3,23 @@ import { inject, injectable } from 'inversify'; import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode'; +import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { Commands, NotebookCellScheme, PYTHON_LANGUAGE } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { TensorBoard } from '../common/utils/localize'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsNotebookExtension } from './helpers'; +import { TensorboardExperiment } from './tensorboarExperiment'; @injectable() export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + private readonly disposables: IDisposable[] = []; + private sendTelemetryOnce = once( sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { trigger: TensorBoardEntrypointTrigger.nbextension, @@ -24,9 +27,22 @@ export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleA }), ); - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return; + } + this.experiment.disposeOnInstallingTensorboard(this); this.activateInternal().ignoreErrors(); } diff --git a/extensions/positron-python/src/client/tensorBoard/serviceRegistry.ts b/extensions/positron-python/src/client/tensorBoard/serviceRegistry.ts index 8d16766f70c..5fedb7b6abf 100644 --- a/extensions/positron-python/src/client/tensorBoard/serviceRegistry.ts +++ b/extensions/positron-python/src/client/tensorBoard/serviceRegistry.ts @@ -10,6 +10,8 @@ import { TensorBoardPrompt } from './tensorBoardPrompt'; import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; import { TerminalWatcher } from './terminalWatcher'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; +import { TensorboardExperiment } from './tensorboarExperiment'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); @@ -32,4 +34,6 @@ export function registerTypes(serviceManager: IServiceManager): void { ); serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); + serviceManager.addSingleton(TensorboardExperiment, TensorboardExperiment); } diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardFileWatcher.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardFileWatcher.ts index 81c62f1f8de..f2f9344d736 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardFileWatcher.ts @@ -2,12 +2,13 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { Disposable, FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IWorkspaceService } from '../common/application/types'; -import { IDisposableRegistry } from '../common/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardExperiment } from './tensorboarExperiment'; @injectable() export class TensorBoardFileWatcher implements IExtensionSingleActivationService { @@ -17,13 +18,26 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService private globPatterns = ['*tfevents*', '*/*tfevents*', '*/*/*tfevents*']; + private readonly disposables: IDisposable[] = []; + constructor( @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(TensorBoardPrompt) private tensorBoardPrompt: TensorBoardPrompt, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - ) {} + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return; + } + this.experiment.disposeOnInstallingTensorboard(this); this.activateInternal().ignoreErrors(); } diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts index cac29b1d7e7..585b9151922 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts @@ -3,15 +3,16 @@ import { inject, injectable } from 'inversify'; import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode'; +import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { Commands, PYTHON } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { TensorBoard } from '../common/utils/localize'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; +import { TensorboardExperiment } from './tensorboarExperiment'; @injectable() export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { @@ -24,9 +25,24 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva }), ); - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} + private readonly disposables: IDisposable[] = []; + + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return; + } + this.experiment.disposeOnInstallingTensorboard(this); this.activateInternal().ignoreErrors(); } diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardPrompt.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardPrompt.ts index 1c03a696dc1..d42101cb51d 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardPrompt.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardPrompt.ts @@ -84,7 +84,7 @@ export class TensorBoardPrompt { } } - private isPromptEnabled(): boolean { + public isPromptEnabled(): boolean { return this.state.value; } diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts index 1d24e8c313f..fb54ad6f32e 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts @@ -100,7 +100,10 @@ export class TensorBoardSession { private readonly globalMemento: IPersistentState, private readonly multiStepFactory: IMultiStepInputFactory, private readonly configurationService: IConfigurationService, - ) {} + ) { + this.disposables.push(this.onDidChangeViewStateEventEmitter); + this.disposables.push(this.onDidDisposeEventEmitter); + } public get onDidDispose(): Event { return this.onDidDisposeEventEmitter.event; @@ -189,10 +192,10 @@ export class TensorBoardSession { // to start a TensorBoard session. If the user has a torch import in // any of their open documents, also try to install the torch-tb-plugin // package, but don't block if installing that fails. - private async ensurePrerequisitesAreInstalled() { + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); const interpreter = - (await this.interpreterService.getActiveInterpreter()) || + (await this.interpreterService.getActiveInterpreter(resource)) || (await this.commandManager.executeCommand('python.setInterpreter')); if (!interpreter) { return false; diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts index 53878bd543c..ec52b9ef94d 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { l10n, ViewColumn } from 'vscode'; +import { Disposable, l10n, ViewColumn } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { Commands } from '../common/constants'; @@ -14,6 +14,7 @@ import { IPersistentState, IPersistentStateFactory, IConfigurationService, + IDisposable, } from '../common/types'; import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; import { IInterpreterService } from '../interpreter/contracts'; @@ -22,8 +23,9 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; +import { TensorboardExperiment } from './tensorboarExperiment'; -const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; +export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @injectable() export class TensorBoardSessionProvider implements IExtensionSingleActivationService { @@ -35,18 +37,22 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer private hasActiveTensorBoardSessionContext: ContextKey; + private readonly disposables: IDisposable[] = []; + constructor( @inject(IInstaller) private readonly installer: IInstaller, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, ) { + disposables.push(this); this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( PREFERRED_VIEWGROUP, ViewColumn.Active, @@ -57,23 +63,36 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer ); } + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } + public async activate(): Promise { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return; + } + this.experiment.disposeOnInstallingTensorboard(this); + this.disposables.push( this.commandManager.registerCommand( Commands.LaunchTensorBoard, ( entrypoint: TensorBoardEntrypoint = TensorBoardEntrypoint.palette, trigger: TensorBoardEntrypointTrigger = TensorBoardEntrypointTrigger.palette, - ) => { + ): void => { sendTelemetryEvent(EventName.TENSORBOARD_SESSION_LAUNCH, undefined, { trigger, entrypoint, }); - return this.createNewSession(); + if (this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension') { + void this.createNewSession(); + } }, ), this.commandManager.registerCommand(Commands.RefreshTensorBoard, () => - this.knownSessions.map((w) => w.refresh()), + this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension' + ? this.knownSessions.map((w) => w.refresh()) + : undefined, ), ); } diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardUsageTracker.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardUsageTracker.ts index 99d82949dcf..d1b21473677 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardUsageTracker.ts @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { TextEditor } from 'vscode'; +import { Disposable, TextEditor } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IDocumentManager } from '../common/application/types'; import { isTestExecution } from '../common/constants'; @@ -12,6 +12,7 @@ import { getDocumentLines } from '../telemetry/importTracker'; import { TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardExperiment } from './tensorboarExperiment'; const testExecution = isTestExecution(); @@ -25,9 +26,18 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @inject(TensorBoardPrompt) private prompt: TensorBoardPrompt, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, ) {} + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } + public async activate(): Promise { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return; + } + this.experiment.disposeOnInstallingTensorboard(this); if (testExecution) { await this.activateInternal(); } else { diff --git a/extensions/positron-python/src/client/tensorBoard/tensorboarExperiment.ts b/extensions/positron-python/src/client/tensorBoard/tensorboarExperiment.ts new file mode 100644 index 00000000000..3cf4cb3c779 --- /dev/null +++ b/extensions/positron-python/src/client/tensorBoard/tensorboarExperiment.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Disposable, EventEmitter, commands, extensions, l10n, window } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { IDisposable, IDisposableRegistry, IExperimentService } from '../common/types'; +import { RecommendTensobardExtension } from '../common/experiments/groups'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; + +@injectable() +export class TensorboardExperiment { + private readonly _onDidChange = new EventEmitter(); + + public readonly onDidChange = this._onDidChange.event; + + private readonly toDisposeWhenTensobardIsInstalled: IDisposable[] = []; + + public static get isTensorboardExtensionInstalled(): boolean { + return !!extensions.getExtension(TENSORBOARD_EXTENSION_ID); + } + + private readonly isExperimentEnabled: boolean; + + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IExperimentService) experiments: IExperimentService, + ) { + this.isExperimentEnabled = experiments.inExperimentSync(RecommendTensobardExtension.experiment); + disposables.push(this._onDidChange); + extensions.onDidChange( + () => + TensorboardExperiment.isTensorboardExtensionInstalled + ? Disposable.from(...this.toDisposeWhenTensobardIsInstalled).dispose() + : undefined, + this, + disposables, + ); + } + + public recommendAndUseNewExtension(): 'continueWithPythonExtension' | 'usingTensorboardExtension' { + if (!this.isExperimentEnabled) { + return 'continueWithPythonExtension'; + } + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return 'usingTensorboardExtension'; + } + const install = l10n.t('Install Tensorboard Extension'); + window + .showInformationMessage( + l10n.t( + 'Install the TensorBoard extension to use the this functionality. Once installed, select the command `Launch Tensorboard`.', + ), + { modal: true }, + install, + ) + .then((result): void => { + if (result === install) { + void commands.executeCommand('workbench.extensions.installExtension', TENSORBOARD_EXTENSION_ID); + } + }); + return 'usingTensorboardExtension'; + } + + public disposeOnInstallingTensorboard(disposabe: IDisposable): void { + this.toDisposeWhenTensobardIsInstalled.push(disposabe); + } +} diff --git a/extensions/positron-python/src/client/tensorBoard/tensorboardDependencyChecker.ts b/extensions/positron-python/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 00000000000..5c377e1d245 --- /dev/null +++ b/extensions/positron-python/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri, ViewColumn } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { + IInstaller, + IPersistentState, + IPersistentStateFactory, + IConfigurationService, + IDisposable, +} from '../common/types'; +import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; +import { disposeAll } from '../common/utils/resourceLifecycle'; +import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; + +@injectable() +export class TensorboardDependencyChecker { + private preferredViewGroupMemento: IPersistentState; + + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( + PREFERRED_VIEWGROUP, + ViewColumn.Active, + ); + } + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const disposables: IDisposable[] = []; + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.workspaceService, + this.pythonExecFactory, + this.commandManager, + disposables, + this.applicationShell, + this.preferredViewGroupMemento, + this.multiStepFactory, + this.configurationService, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + disposeAll(disposables); + return result; + } +} diff --git a/extensions/positron-python/src/client/tensorBoard/tensorboardIntegration.ts b/extensions/positron-python/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 00000000000..22d590d6ee6 --- /dev/null +++ b/extensions/positron-python/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,99 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri, commands } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IDisposableRegistry, IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +type PythonApiForTensorboardExtension = { + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + ) { + this.hideCommands(); + extensions.onDidChange(this.hideCommands, this, disposables); + } + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + this.hideCommands(); + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public hideCommands(): void { + if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { + void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); + } + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/extensions/positron-python/src/client/tensorBoard/terminalWatcher.ts b/extensions/positron-python/src/client/tensorBoard/terminalWatcher.ts index 5aadc12dc4c..5f48def54e4 100644 --- a/extensions/positron-python/src/client/tensorBoard/terminalWatcher.ts +++ b/extensions/positron-python/src/client/tensorBoard/terminalWatcher.ts @@ -4,6 +4,7 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { TensorboardExperiment } from './tensorboarExperiment'; // Every 5 min look, through active terminals to see if any are running `tensorboard` @injectable() @@ -12,9 +13,18 @@ export class TerminalWatcher implements IExtensionSingleActivationService, IDisp private handle: NodeJS.Timeout | undefined; - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} + constructor( + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } public async activate(): Promise { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return; + } + this.experiment.disposeOnInstallingTensorboard(this); const handle = setInterval(() => { // When user runs a command in VSCode terminal, the terminal's name // becomes the program that is currently running. Since tensorboard diff --git a/extensions/positron-python/src/client/tensorBoard/types.ts b/extensions/positron-python/src/client/tensorBoard/types.ts index 6e2c274d63f..a11659015da 100644 --- a/extensions/positron-python/src/client/tensorBoard/types.ts +++ b/extensions/positron-python/src/client/tensorBoard/types.ts @@ -1,9 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event } from 'vscode'; +import { Event, Uri } from 'vscode'; export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); export interface ITensorBoardImportTracker { onDidImportTensorBoard: Event; } + +export const ITensorboardDependencyChecker = Symbol('ITensorboardDependencyChecker'); +export interface ITensorboardDependencyChecker { + ensureDependenciesAreInstalled(resource?: Uri): Promise; +} diff --git a/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts b/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts index 37ddea16989..bc559906143 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -214,7 +214,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!, wholeFileContent); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } diff --git a/extensions/positron-python/src/client/terminals/codeExecution/helper.ts b/extensions/positron-python/src/client/terminals/codeExecution/helper.ts index 0d5694b4a28..058c78e332a 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/helper.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/helper.ts @@ -5,7 +5,12 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; @@ -14,7 +19,10 @@ import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; import { traceError } from '../../logging'; -import { Resource } from '../../common/types'; +import { IConfigurationService, IExperimentService, Resource } from '../../common/types'; +import { EnableREPLSmartSend } from '../../common/experiments/groups'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { @@ -26,14 +34,22 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly interpreterService: IInterpreterService; + private readonly commandManager: ICommandManager; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read. + private readonly configSettings: IConfigurationService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); this.interpreterService = serviceContainer.get(IInterpreterService); + this.configSettings = serviceContainer.get(IConfigurationService); + this.commandManager = serviceContainer.get(ICommandManager); } - public async normalizeLines(code: string, resource?: Uri): Promise { + public async normalizeLines(code: string, wholeFileContent?: string, resource?: Uri): Promise { try { if (code.trim().length === 0) { return ''; @@ -42,6 +58,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // So just remove cr from the input. code = code.replace(new RegExp('\\r', 'g'), ''); + const activeEditor = this.documentManager.activeTextEditor; const interpreter = await this.interpreterService.getActiveInterpreter(resource); const processService = await this.processServiceFactory.create(resource); @@ -63,10 +80,24 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { normalizeOutput.resolve(normalized); }, }); - + // If there is no explicit selection, we are exeucting 'line' or 'block'. + if (activeEditor?.selection?.isEmpty) { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' }); + } // The normalization script expects a serialized JSON object, with the selection under the "code" key. // We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters. - const input = JSON.stringify({ code }); + const startLineVal = activeEditor?.selection?.start.line ?? 0; + const endLineVal = activeEditor?.selection?.end.line ?? 0; + const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; + const smartSendExperimentEnabledVal = pythonSmartSendEnabled(this.serviceContainer); + const input = JSON.stringify({ + code, + wholeFileContent, + startLine: startLineVal, + endLine: endLineVal, + emptyHighlight: emptyHighlightVal, + smartSendExperimentEnabled: smartSendExperimentEnabledVal, + }); observable.proc?.stdin?.write(input); observable.proc?.stdin?.end(); @@ -74,6 +105,11 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const result = await normalizeOutput.promise; const object = JSON.parse(result); + if (activeEditor?.selection) { + const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; + await this.moveToNextBlock(lineOffset, activeEditor); + } + return parse(object.normalized); } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); @@ -81,6 +117,30 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } } + /** + * Depending on whether or not user is in experiment for smart send, + * dynamically move the cursor to the next block of code. + * The cursor movement is not moved by one everytime, + * since with the smart selection, the next executable code block + * can be multiple lines away. + * Intended to provide smooth shift+enter user experience + * bringing user's cursor to the next executable block of code when used with smart selection. + */ + // eslint-disable-next-line class-methods-use-this + private async moveToNextBlock(lineOffset: number, activeEditor?: TextEditor): Promise { + if (pythonSmartSendEnabled(this.serviceContainer)) { + if (activeEditor?.selection?.isEmpty) { + await this.commandManager.executeCommand('cursorMove', { + to: 'down', + by: 'line', + value: Number(lineOffset), + }); + await this.commandManager.executeCommand('cursorEnd'); + } + } + return Promise.resolve(); + } + public async getFileToExecute(): Promise { const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { @@ -92,7 +152,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { return undefined; } if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file)')); + this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file')); return undefined; } if (activeEditor.document.isDirty) { @@ -110,6 +170,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const { selection } = textEditor; let code: string; + if (selection.isEmpty) { code = textEditor.document.lineAt(selection.start.line).text; } else if (selection.isSingleLine) { @@ -117,6 +178,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } else { code = getMultiLineSelectionText(textEditor); } + return code; } @@ -235,3 +297,9 @@ function getMultiLineSelectionText(textEditor: TextEditor): string { // ↑<---------------- To here return selectionText; } + +function pythonSmartSendEnabled(serviceContainer: IServiceContainer): boolean { + const experiment = serviceContainer.get(IExperimentService); + + return experiment ? experiment.inExperimentSync(EnableREPLSmartSend.experiment) : false; +} diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts new file mode 100644 index 00000000000..a9fd804291a --- /dev/null +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivatePrompt.ts @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { + Position, + Uri, + WorkspaceEdit, + Range, + TextEditorRevealType, + ProgressLocation, + Terminal, + Selection, +} from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + IDocumentManager, + ITerminalManager, +} from '../../common/application/types'; +import { IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { traceError } from '../../logging'; +import { shellExec } from '../../common/process/rawProcessApis'; +import { sleep } from '../../common/utils/async'; +import { getDeactivateShellInfo } from './deactivateScripts'; +import { isTestExecution } from '../../common/constants'; +import { ProgressService } from '../../common/application/progressService'; +import { copyFile, createFile, pathExists } from '../../common/platform/fs-paths'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; +@injectable() +export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + private terminalProcessId: number | undefined; + + private readonly progressService: ProgressService; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) { + this.progressService = new ProgressService(this.appShell); + } + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(6000); + } + this.disposableRegistry.push( + this.appShell.onDidWriteTerminalData(async (e) => { + if (!e.data.includes('deactivate')) { + return; + } + let shellType = identifyShellFromShellPath(this.appEnvironment.shell); + if (shellType === TerminalShellType.commandPrompt) { + return; + } + if (getOSType() === OSType.OSX && shellType === TerminalShellType.bash) { + // On macOS, sometimes bash is overriden by OS to actually launch zsh, so we need to execute inside + // the shell to get the correct shell type. + const shell = await shellExec('echo $SHELL', { shell: this.appEnvironment.shell }).then((output) => + output.stdout.trim(), + ); + shellType = identifyShellFromShellPath(shell); + } + const { terminal } = e; + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : undefined; + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return; + } + await this._notifyUsers(shellType, terminal).catch((ex) => traceError('Deactivate prompt failed', ex)); + }), + ); + } + + public async _notifyUsers(shellType: TerminalShellType, terminal: Terminal): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + `${terminalDeactivationPromptKey}-${shellType}`, + true, + ); + if (!notificationPromptEnabled.value) { + const processId = await terminal.processId; + if (processId && this.terminalProcessId === processId) { + // Existing terminal needs to be restarted for changes to take effect. + await this.forceRestartShell(terminal); + } + return; + } + const scriptInfo = getDeactivateShellInfo(shellType); + if (!scriptInfo) { + // Shell integration is not supported for these shells, in which case this workaround won't work. + return; + } + const telemetrySelections: ['Edit script', "Don't show again"] = ['Edit script', "Don't show again"]; + const { initScript, source, destination } = scriptInfo; + const prompts = [Common.editSomething.format(initScript.displayName), Common.doNotShowAgain]; + const selection = await this.appShell.showWarningMessage( + Interpreters.terminalDeactivatePrompt.format(initScript.displayName), + ...prompts, + ); + let index = selection ? prompts.indexOf(selection) : 0; + if (selection === prompts[0]) { + index = 0; + } + sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { + selection: selection ? telemetrySelections[index] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.terminalDeactivateProgress.format(initScript.displayName), + }); + await copyFile(source, destination); + await this.openScriptWithEdits(initScript.command, initScript.contents); + await notificationPromptEnabled.updateValue(false); + this.progressService.hideProgress(); + this.terminalProcessId = await terminal.processId; + } + if (selection === prompts[1]) { + await notificationPromptEnabled.updateValue(false); + } + } + + private async openScriptWithEdits(command: string, content: string) { + const document = await this.openScript(command); + const hookMarker = 'VSCode venv deactivate hook'; + content = ` +# >>> ${hookMarker} >>> +${content} +# <<< ${hookMarker} <<<`; + // If script already has the hook, don't add it again. + const editor = await this.documentManager.showTextDocument(document); + if (document.getText().includes(hookMarker)) { + editor.revealRange( + new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), + TextEditorRevealType.AtTop, + ); + return; + } + const editorEdit = new WorkspaceEdit(); + editorEdit.insert(document.uri, new Position(document.lineCount, 0), content); + await this.documentManager.applyEdit(editorEdit); + // Reveal the edits. + editor.selection = new Selection(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)); + editor.revealRange( + new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), + TextEditorRevealType.AtTop, + ); + } + + private async openScript(command: string) { + const initScriptPath = await this.getPathToScript(command); + if (!(await pathExists(initScriptPath))) { + await createFile(initScriptPath); + } + const document = await this.documentManager.openTextDocument(initScriptPath); + return document; + } + + private async getPathToScript(command: string) { + return shellExec(command, { shell: this.appEnvironment.shell }).then((output) => output.stdout.trim()); + } + + public async forceRestartShell(terminal: Terminal): Promise { + terminal.dispose(); + terminal = this.terminalManager.createTerminal({ + message: Interpreters.restartingTerminal, + }); + terminal.show(true); + terminal.sendText('deactivate'); + } +} diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts new file mode 100644 index 00000000000..34917e44bbd --- /dev/null +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/deactivateScripts.ts @@ -0,0 +1,108 @@ +/* eslint-disable no-case-declarations */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; +import { TerminalShellType } from '../../common/terminal/types'; + +type DeactivateShellInfo = { + /** + * Full path to source deactivate script to copy. + */ + source: string; + /** + * Full path to destination to copy deactivate script to. + */ + destination: string; + initScript: { + /** + * Display name of init script for the shell. + */ + displayName: string; + /** + * Command to run in shell to output the full path to init script. + */ + command: string; + /** + * Contents to add to init script. + */ + contents: string; + }; +}; + +// eslint-disable-next-line global-require +const untildify: (value: string) => string = require('untildify'); + +export function getDeactivateShellInfo(shellType: TerminalShellType): DeactivateShellInfo | undefined { + switch (shellType) { + case TerminalShellType.bash: + return buildInfo( + 'deactivate', + { + displayName: '~/.bashrc', + path: '~/.bashrc', + }, + `source {0}`, + ); + case TerminalShellType.powershellCore: + case TerminalShellType.powershell: + return buildInfo( + 'deactivate.ps1', + { + displayName: 'Powershell Profile', + path: '$Profile', + }, + `& "{0}"`, + ); + case TerminalShellType.zsh: + return buildInfo( + 'deactivate', + { + displayName: '~/.zshrc', + path: '~/.zshrc', + }, + `source {0}`, + ); + case TerminalShellType.fish: + return buildInfo( + 'deactivate.fish', + { + displayName: 'config.fish', + path: '$__fish_config_dir/config.fish', + }, + `source {0}`, + ); + case TerminalShellType.cshell: + return buildInfo( + 'deactivate.csh', + { + displayName: '~/.cshrc', + path: '~/.cshrc', + }, + `source {0}`, + ); + default: + return undefined; + } +} + +function buildInfo( + deactivate: string, + initScript: { + path: string; + displayName: string; + }, + scriptCommandFormat: string, +) { + const scriptPath = path.join('~', '.vscode-python', deactivate); + return { + source: path.join(_SCRIPTS_DIR, deactivate), + destination: untildify(scriptPath), + initScript: { + displayName: initScript.displayName, + command: `echo ${initScript.path}`, + contents: scriptCommandFormat.format(scriptPath), + }, + }; +} diff --git a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/indicatorPrompt.ts similarity index 71% rename from extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts rename to extensions/positron-python/src/client/terminals/envCollectionActivation/indicatorPrompt.ts index c8aea205a32..3e463e38654 100644 --- a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri, l10n } from 'vscode'; +import { Uri } from 'vscode'; import * as path from 'path'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; import { @@ -14,15 +14,18 @@ import { } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { sleep } from '../../common/utils/async'; +import { isTestExecution } from '../../common/constants'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @injectable() -export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( @@ -42,8 +45,19 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio if (!inTerminalEnvVarExperiment(this.experimentService)) { return; } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(6000); + } this.disposableRegistry.push( this.terminalManager.onDidOpenTerminal(async (terminal) => { + const hideFromUser = + 'hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser; + const strictEnv = 'strictEnv' in terminal.creationOptions && terminal.creationOptions.strictEnv; + if (hideFromUser || strictEnv || terminal.creationOptions.name) { + // Only show this notification for basic terminals created using the '+' button. + return; + } const cwd = 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd ? terminal.creationOptions.cwd @@ -72,9 +86,13 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio } const prompts = [Common.doNotShowAgain]; const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter || !interpreter.type) { + return; + } const terminalPromptName = getPromptName(interpreter); + const environmentType = interpreter.type === PythonEnvType.Conda ? 'Selected conda' : 'Python virtual'; const selection = await this.appShell.showInformationMessage( - Interpreters.terminalEnvVarCollectionPrompt.format(terminalPromptName), + Interpreters.terminalEnvVarCollectionPrompt.format(environmentType, terminalPromptName), ...prompts, ); if (!selection) { @@ -86,15 +104,12 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio } } -function getPromptName(interpreter?: PythonEnvironment) { - if (!interpreter) { - return ''; - } +function getPromptName(interpreter: PythonEnvironment) { if (interpreter.envName) { - return `, ${l10n.t('i.e')} "(${interpreter.envName})"`; + return `"(${interpreter.envName})"`; } if (interpreter.envPath) { - return `, ${l10n.t('i.e')} "(${path.basename(interpreter.envPath)})"`; + return `"(${path.basename(interpreter.envPath)})"`; } - return ''; + return 'environment indicator'; } diff --git a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts similarity index 83% rename from extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts rename to extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts index c11ec221d4d..46e70d60d92 100644 --- a/extensions/positron-python/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/service.ts @@ -4,12 +4,12 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; import { - ProgressOptions, - ProgressLocation, MarkdownString, WorkspaceFolder, GlobalEnvironmentVariableCollection, EnvironmentVariableScope, + EnvironmentVariableMutatorOptions, + ProgressLocation, } from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; @@ -25,12 +25,11 @@ import { IConfigurationService, IPathUtils, } from '../../common/types'; -import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; -import { IInterpreterService } from '../contracts'; -import { defaultShells } from './service'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; +import { traceError, traceVerbose, traceWarn } from '../../logging'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; @@ -38,6 +37,9 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { ShellIntegrationShells } from './shellIntegration'; +import { ProgressService } from '../../common/application/progressService'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -55,8 +57,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ TerminalShellType.fish, ]; - private deferred: Deferred | undefined; - private registeredOnce = false; /** @@ -64,6 +64,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ */ private processEnvVars: EnvironmentVariables | undefined; + private readonly progressService: ProgressService; + private separator: string; constructor( @@ -80,6 +82,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IPathUtils) private readonly pathUtils: IPathUtils, ) { this.separator = platform.osType === OSType.Windows ? ';' : ':'; + this.progressService = new ProgressService(this.shell); } public async activate(resource: Resource): Promise { @@ -117,6 +120,12 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this, this.disposables, ); + const { shell } = this.applicationEnvironment; + const isActive = this.isShellIntegrationActive(shell); + const shellType = identifyShellFromShellPath(shell); + if (!isActive && shellType !== TerminalShellType.commandPrompt) { + traceWarn(`Shell integration is not active, environment activated maybe overriden by the shell.`); + } this.registeredOnce = true; } this._applyCollection(resource).ignoreErrors(); @@ -126,9 +135,12 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } public async _applyCollection(resource: Resource, shell?: string): Promise { - this.showProgress(); + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }); await this._applyCollectionImpl(resource, shell); - this.hideProgress(); + this.progressService.hideProgress(); } private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { @@ -147,13 +159,14 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ shell, ); const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; + traceVerbose(`Activated environment variables for ${resource?.fsPath}`, env); if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; if (defaultShell?.shellType !== shellType) { // Commands to fetch env vars may fail in custom shells due to unknown reasons, in that case // fallback to default shells as they are known to work better. - await this._applyCollection(resource, defaultShell?.shell); + await this._applyCollectionImpl(resource, defaultShell?.shell); return; } await this.trackTerminalPrompt(shell, resource, env); @@ -171,6 +184,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); + const prependOptions = this.getPrependOptions(shell); // Clear any previously set env vars from collection envVarCollection.clear(); @@ -185,10 +199,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (key === 'PS1') { // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); - envVarCollection.prepend(key, value, { - applyAtShellIntegration: true, - applyAtProcessCreation: false, - }); + envVarCollection.prepend(key, value, prependOptions); return; } if (key === 'PATH') { @@ -198,19 +209,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const prependedPart = env.PATH.slice(0, -processEnv.PATH.length); value = prependedPart; traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); - envVarCollection.prepend(key, value, { - applyAtShellIntegration: true, - applyAtProcessCreation: true, - }); + envVarCollection.prepend(key, value, prependOptions); } else { if (!value.endsWith(this.separator)) { value = value.concat(this.separator); } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); - envVarCollection.prepend(key, value, { - applyAtShellIntegration: true, - applyAtProcessCreation: true, - }); + envVarCollection.prepend(key, value, prependOptions); } return; } @@ -272,9 +277,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 should be set but no PS1 was set. return; } - const config = this.workspaceService - .getConfiguration('terminal') - .get('integrated.shellIntegration.enabled'); + const config = this.isShellIntegrationActive(shell); if (!config) { traceVerbose('PS1 is not set when shell integration is disabled.'); return; @@ -329,6 +332,36 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } + private getPrependOptions(shell: string): EnvironmentVariableMutatorOptions { + const isActive = this.isShellIntegrationActive(shell); + // Ideally we would want to prepend exactly once, either at shell integration or process creation. + // TODO: Stop prepending altogether once https://github.com/microsoft/vscode/issues/145234 is available. + return isActive + ? { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + } + : { + applyAtShellIntegration: true, // Takes care of false negatives in case manual integration is being used. + applyAtProcessCreation: true, + }; + } + + private isShellIntegrationActive(shell: string): boolean { + const isEnabled = this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled')!; + if (isEnabled && ShellIntegrationShells.includes(identifyShellFromShellPath(shell))) { + // Unfortunately shell integration could still've failed in remote scenarios, we can't know for sure: + // https://code.visualstudio.com/docs/terminal/shell-integration#_automatic-script-injection + return true; + } + if (!isEnabled) { + traceVerbose('Shell integrated is disabled in user settings.'); + } + return false; + } + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; return envVarCollection.getScoped(scope); @@ -345,32 +378,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } return workspaceFolder; } - - @traceDecoratorVerbose('Display activating terminals') - private showProgress(): void { - if (!this.deferred) { - this.createProgress(); - } - } - - @traceDecoratorVerbose('Hide activating terminals') - private hideProgress(): void { - if (this.deferred) { - this.deferred.resolve(); - this.deferred = undefined; - } - } - - private createProgress() { - const progressOptions: ProgressOptions = { - location: ProgressLocation.Window, - title: Interpreters.activatingTerminals, - }; - this.shell.withProgress(progressOptions, () => { - this.deferred = createDeferred(); - return this.deferred.promise; - }); - } } function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { @@ -394,7 +401,12 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl } function shouldSkip(env: string) { - return ['_', 'SHLVL'].includes(env); + return [ + '_', + 'SHLVL', + // Even though this maybe returned, setting it can result in output encoding errors in terminal. + 'PYTHONUTF8', + ].includes(env); } function getPromptForEnv(interpreter: PythonEnvironment | undefined) { diff --git a/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts b/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts new file mode 100644 index 00000000000..1be2501595a --- /dev/null +++ b/extensions/positron-python/src/client/terminals/envCollectionActivation/shellIntegration.ts @@ -0,0 +1,13 @@ +import { TerminalShellType } from '../../common/terminal/types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +export const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; diff --git a/extensions/positron-python/src/client/terminals/serviceRegistry.ts b/extensions/positron-python/src/client/terminals/serviceRegistry.ts index a39ef31a8fe..a9da776d011 100644 --- a/extensions/positron-python/src/client/terminals/serviceRegistry.ts +++ b/extensions/positron-python/src/client/terminals/serviceRegistry.ts @@ -1,25 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { interfaces } from 'inversify'; -import { ClassType } from '../ioc/types'; +import { IServiceManager } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation, + ITerminalEnvVarCollectionService, +} from './types'; +import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; +import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; -interface IServiceRegistry { - addSingleton( - serviceIdentifier: interfaces.ServiceIdentifier, - constructor: ClassType, - name?: string | number | symbol, - ): void; -} - -export function registerTypes(serviceManager: IServiceRegistry): void { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -37,4 +38,17 @@ export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalDeactivateLimitationPrompt, + ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/extensions/positron-python/src/client/terminals/types.ts b/extensions/positron-python/src/client/terminals/types.ts index 47ac16d9e08..ba30b8f6d47 100644 --- a/extensions/positron-python/src/client/terminals/types.ts +++ b/extensions/positron-python/src/client/terminals/types.ts @@ -15,7 +15,7 @@ export interface ICodeExecutionService { export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise; + normalizeLines(code: string, wholeFileContent?: string): Promise; getFileToExecute(): Promise; saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; @@ -33,3 +33,11 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/extensions/positron-python/src/client/testing/common/debugLauncher.ts b/extensions/positron-python/src/client/testing/common/debugLauncher.ts index 63e2a4543be..c76557699ff 100644 --- a/extensions/positron-python/src/client/testing/common/debugLauncher.ts +++ b/extensions/positron-python/src/client/testing/common/debugLauncher.ts @@ -205,19 +205,13 @@ export class DebugLauncher implements ITestDebugLauncher { } launchArgs.request = 'launch'; - // Both types of tests need to have the port for the test result server. - if (options.runTestIdsPort) { - launchArgs.env = { - ...launchArgs.env, - RUN_TEST_IDS_PORT: options.runTestIdsPort, - }; - } - if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { - if (options.pytestPort && options.pytestUUID) { + if (pythonTestAdapterRewriteExperiment) { + if (options.pytestPort && options.pytestUUID && options.runTestIdsPort) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, + RUN_TEST_IDS_PORT: options.runTestIdsPort, }; } else { throw Error( diff --git a/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts b/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts index 79cee6452a8..15efc7aa4bb 100644 --- a/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts +++ b/extensions/positron-python/src/client/testing/testController/common/resultResolver.ts @@ -1,7 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; +import { + CancellationToken, + TestController, + TestItem, + Uri, + TestMessage, + Location, + TestRun, + MarkdownString, +} from 'vscode'; import * as util from 'util'; import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; @@ -11,7 +20,7 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; +import { buildErrorNodeOptions, populateTestTree, splitTestNameWithRegex } from './utils'; import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { @@ -65,7 +74,7 @@ export class PythonResultResolver implements ITestResultResolver { const testingErrorConst = this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; const { error } = rawTestData; - traceError(testingErrorConst, '\r\n', error?.join('\r\n\r\n') ?? ''); + traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); const message = util.format( @@ -78,7 +87,11 @@ export class PythonResultResolver implements ITestResultResolver { errorNode = createErrorTestItem(this.testController, options); this.testController.items.add(errorNode); } - errorNode.error = message; + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; } else { // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); @@ -90,14 +103,6 @@ export class PythonResultResolver implements ITestResultResolver { // If the test root for this folder exists: Workspace refresh, update its children. // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. populateTestTree(this.testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); - this.testController.items.replace([]); - // Add back the error node if it exists. - if (errorNode !== undefined) { - this.testController.items.add(errorNode); - } } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { @@ -138,16 +143,17 @@ export class PythonResultResolver implements ITestResultResolver { const tempArr: TestItem[] = getTestCaseNodes(i); testCases.push(...tempArr); }); + const testItem = rawTestExecData.result[keyTemp]; - if (rawTestExecData.result[keyTemp].outcome === 'error') { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + if (testItem.outcome === 'error') { + const rawTraceback = testItem.traceback ?? ''; const traceback = splitLines(rawTraceback, { trim: false, removeEmptyEntries: true, }).join('\r\n'); - const text = `${rawTestExecData.result[keyTemp].test} failed with error: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; + const text = `${testItem.test} failed with error: ${ + testItem.message ?? testItem.outcome + }\r\n${traceback}`; const message = new TestMessage(text); const grabVSid = this.runIdToVSid.get(keyTemp); @@ -157,23 +163,17 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.uri && indiItem.range) { message.location = new Location(indiItem.uri, indiItem.range); runInstance.errored(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); } } }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' - ) { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + const rawTraceback = testItem.traceback ?? ''; const traceback = splitLines(rawTraceback, { trim: false, removeEmptyEntries: true, }).join('\r\n'); - const text = `${rawTestExecData.result[keyTemp].test} failed: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; const message = new TestMessage(text); // note that keyTemp is a runId for unittest library... @@ -184,14 +184,10 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.uri && indiItem.range) { message.location = new Location(indiItem.uri, indiItem.range); runInstance.failed(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); } } }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' - ) { + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); if (grabTestItem !== undefined) { @@ -203,7 +199,7 @@ export class PythonResultResolver implements ITestResultResolver { } }); } - } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { + } else if (testItem.outcome === 'skipped') { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); if (grabTestItem !== undefined) { @@ -215,11 +211,11 @@ export class PythonResultResolver implements ITestResultResolver { } }); } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { + } else if (testItem.outcome === 'subtest-failure') { // split on [] or () based on how the subtest is setup. const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = rawTestExecData.result[keyTemp]; + const data = testItem; // find the subtest's parent test item if (parentTestItem) { const subtestStats = this.subTestStats.get(parentTestCaseId); @@ -230,20 +226,19 @@ export class PythonResultResolver implements ITestResultResolver { failed: 1, passed: 0, }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } const subTestItem = this.testController?.createTestItem(subtestId, subtestId); - runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); // create a new test item for the subtest if (subTestItem) { const traceback = data.traceback ?? ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); + const text = `${data.subtest} failed: ${ + testItem.message ?? testItem.outcome + }\r\n${traceback}`; parentTestItem.children.add(subTestItem); runInstance.started(subTestItem); - const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); + const message = new TestMessage(text); if (parentTestItem.uri && parentTestItem.range) { message.location = new Location(parentTestItem.uri, parentTestItem.range); } @@ -254,7 +249,7 @@ export class PythonResultResolver implements ITestResultResolver { } else { throw new Error('Parent test item not found'); } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { + } else if (testItem.outcome === 'subtest-success') { // split on [] or () based on how the subtest is setup. const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); @@ -266,7 +261,6 @@ export class PythonResultResolver implements ITestResultResolver { subtestStats.passed += 1; } else { this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } @@ -276,7 +270,6 @@ export class PythonResultResolver implements ITestResultResolver { parentTestItem.children.add(subTestItem); runInstance.started(subTestItem); runInstance.passed(subTestItem); - runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); } else { throw new Error('Unable to create new child node for subtest'); } diff --git a/extensions/positron-python/src/client/testing/testController/common/server.ts b/extensions/positron-python/src/client/testing/testController/common/server.ts index 46217eab045..5969a5f7570 100644 --- a/extensions/positron-python/src/client/testing/testController/common/server.ts +++ b/extensions/positron-python/src/client/testing/testController/common/server.ts @@ -17,12 +17,15 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { + MESSAGE_ON_TESTING_OUTPUT_MOVE, createDiscoveryErrorPayload, createEOTPayload, createExecutionErrorPayload, extractJsonPayload, + fixLogLinesNoTrailing, } from './utils'; import { createDeferred } from '../../../common/utils/async'; +import { EnvironmentVariables } from '../../../api/types'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -41,6 +44,7 @@ export class PythonTestServer implements ITestServer, Disposable { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { + traceVerbose('data received from python server: ', data.toString()); buffer = Buffer.concat([buffer, data]); // get the new data and add it to the buffer while (buffer.length > 0) { try { @@ -89,6 +93,10 @@ export class PythonTestServer implements ITestServer, Disposable { // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. traceVerbose(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); + } else { + traceVerbose( + `extract json payload incomplete, uuid= ${extractedJsonPayload.uuid} and cleanedJsonData= ${extractedJsonPayload.cleanedJsonData}`, + ); } buffer = Buffer.from(extractedJsonPayload.remainingRawData); if (buffer.length === 0) { @@ -165,28 +173,30 @@ export class PythonTestServer implements ITestServer, Disposable { async sendCommand( options: TestCommandOptions, + env: EnvironmentVariables, runTestIdPort?: string, runInstance?: TestRun, testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; - - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const isDiscovery = (testIds === undefined || testIds.length === 0) && runTestIdPort === undefined; + const mutableEnv = { ...env }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.getPort().toString(); + mutableEnv.RUN_TEST_IDS_PORT = runTestIdPort; + const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.getPort().toString(), - }, + env: mutableEnv, }; - - if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { @@ -194,7 +204,6 @@ export class PythonTestServer implements ITestServer, Disposable { resource: options.workspaceFolder, }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - const args = [options.command.script].concat(options.command.args); if (options.outChannel) { @@ -209,8 +218,10 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, testProvider: UNITTEST_PROVIDER, runTestIdsPort: runTestIdPort, + pytestUUID: uuid.toString(), + pytestPort: this.getPort().toString(), }; - traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); + traceInfo(`Running DEBUG unittest for workspace ${options.cwd} with arguments: ${args}\r\n`); await this.debugLauncher!.launchDebugger(launchOptions, () => { callback?.(); @@ -218,17 +229,17 @@ export class PythonTestServer implements ITestServer, Disposable { } else { if (isRun) { // This means it is running the test - traceInfo(`Running unittests with arguments: ${args}\r\n`); + traceInfo(`Running unittests for workspace ${options.cwd} with arguments: ${args}\r\n`); } else { // This means it is running discovery - traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); + traceLog(`Discovering unittest tests for workspace ${options.cwd} with arguments: ${args}\r\n`); } const deferredTillExecClose = createDeferred>(); let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { - traceInfo('Test run cancelled, killing unittest subprocess.'); + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${options.cwd}.`); // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. if (resultProc) { resultProc?.kill(); @@ -240,25 +251,57 @@ export class PythonTestServer implements ITestServer, Disposable { const result = execService?.execObservable(args, spawnOptions); resultProc = result?.proc; - // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - result?.proc?.stdout?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data.toString()); - }); - result?.proc?.stderr?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data.toString()); - }); - result?.proc?.on('exit', (code, signal) => { - if (code !== 0) { - traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); - } - }); + // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. + // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. + if (isDiscovery) { + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + spawnOptions?.outputChannel?.append(`${out}`); + traceInfo(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + spawnOptions?.outputChannel?.append(`${out}`); + traceError(out); + }); + } else { + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(`${out}`); + spawnOptions?.outputChannel?.append(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(`${out}`); + spawnOptions?.outputChannel?.append(out); + }); + } result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - if (code !== 0 && testIds && testIds?.length !== 0) { + spawnOptions?.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); + if (isDiscovery) { + if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${options.cwd}. Creating and sending error discovery payload \n`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + } else if (code !== 0 && testIds) { + // This occurs when we are running the test and there is an error which occurs. + traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} for workspace ${options.cwd}. Creating and sending error execution payload \n`, ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this._onRunDataReceived.fire({ @@ -270,27 +313,13 @@ export class PythonTestServer implements ITestServer, Disposable { uuid, data: JSON.stringify(createEOTPayload(true)), }); - } else if (code !== 0) { - // This occurs when we are running discovery - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, - ); - this._onDiscoveryDataReceived.fire({ - uuid, - data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), - }); - // then send a EOT payload - this._onDiscoveryDataReceived.fire({ - uuid, - data: JSON.stringify(createEOTPayload(true)), - }); } - deferredTillExecClose.resolve({ stdout: '', stderr: '' }); + deferredTillExecClose.resolve(); }); await deferredTillExecClose.promise; } } catch (ex) { - traceError(`Error while server attempting to run unittest command: ${ex}`); + traceError(`Error while server attempting to run unittest command for workspace ${options.cwd}: ${ex}`); this.uuids = this.uuids.filter((u) => u !== uuid); this._onDataReceived.fire({ uuid, diff --git a/extensions/positron-python/src/client/testing/testController/common/types.ts b/extensions/positron-python/src/client/testing/testController/common/types.ts index 32e0c4ba8cc..e51270eb4f9 100644 --- a/extensions/positron-python/src/client/testing/testController/common/types.ts +++ b/extensions/positron-python/src/client/testing/testController/common/types.ts @@ -15,6 +15,7 @@ import { import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; import { Deferred } from '../../../common/utils/async'; +import { EnvironmentVariables } from '../../../common/variables/types'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -177,6 +178,7 @@ export interface ITestServer { readonly onDiscoveryDataReceived: Event; sendCommand( options: TestCommandOptions, + env: EnvironmentVariables, runTestIdsPort?: string, runInstance?: TestRun, testIds?: string[], diff --git a/extensions/positron-python/src/client/testing/testController/common/utils.ts b/extensions/positron-python/src/client/testing/testController/common/utils.ts index f5f416529c4..23ee881a405 100644 --- a/extensions/positron-python/src/client/testing/testController/common/utils.ts +++ b/extensions/positron-python/src/client/testing/testController/common/utils.ts @@ -23,6 +23,11 @@ export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } + +export function fixLogLinesNoTrailing(content: string): string { + const lines = content.split(/\r?\n/g); + return `${lines.join('\r\n')}`; +} export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; @@ -42,6 +47,11 @@ export interface ExtractOutput { export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; +export const MESSAGE_ON_TESTING_OUTPUT_MOVE = + 'Starting now, all test run output will be sent to the Test Result panel,' + + ' while test discovery output will be sent to the "Python" output channel instead of the "Python Test Log" channel.' + + ' The "Python Test Log" channel will be deprecated within the next month.' + + ' See https://github.com/microsoft/vscode-python/wiki/New-Method-for-Output-Handling-in-Python-Testing for details.'; export function createTestingDeferred(): Deferred { return createDeferred(); @@ -287,7 +297,7 @@ export function createExecutionErrorPayload( const etp: ExecutionTestPayload = { cwd, status: 'error', - error: 'Test run failed, the python test process was terminated before it could exit on its own.', + error: `Test run failed, the python test process was terminated before it could exit on its own for workspace ${cwd}`, result: {}, }; // add error result for each attempted test. @@ -311,7 +321,7 @@ export function createDiscoveryErrorPayload( cwd, status: 'error', error: [ - ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal} for workspace ${cwd}`, ], }; } diff --git a/extensions/positron-python/src/client/testing/testController/controller.ts b/extensions/positron-python/src/client/testing/testController/controller.ts index af77ab2b252..bc9d2ca8299 100644 --- a/extensions/positron-python/src/client/testing/testController/controller.ts +++ b/extensions/positron-python/src/client/testing/testController/controller.ts @@ -50,6 +50,7 @@ import { ITestDebugLauncher } from '../common/types'; import { IServiceContainer } from '../../ioc/types'; import { PythonResultResolver } from './common/resultResolver'; import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -100,6 +101,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -174,12 +176,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); executionAdapter = new UnittestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); } else { testProvider = PYTEST_PROVIDER; @@ -189,12 +193,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); executionAdapter = new PytestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); } @@ -263,13 +269,20 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (settings.testing.pytestEnabled) { if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { traceInfo(`Running discovery for pytest using the new test adapter.`); - const testAdapter = - this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - testAdapter.discoverTests( - this.testController, - this.refreshCancellation.token, - this.pythonExecFactory, - ); + if (workspace && workspace.uri) { + const testAdapter = this.testAdapters.get(workspace.uri); + if (testAdapter) { + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.pythonExecFactory, + ); + } else { + traceError('Unable to find test adapter for workspace.'); + } + } else { + traceError('Unable to find workspace for given file'); + } } else { // else use OLD test discovery mechanism await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); @@ -277,13 +290,20 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } else if (settings.testing.unittestEnabled) { if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { traceInfo(`Running discovery for unittest using the new test adapter.`); - const testAdapter = - this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - testAdapter.discoverTests( - this.testController, - this.refreshCancellation.token, - this.pythonExecFactory, - ); + if (workspace && workspace.uri) { + const testAdapter = this.testAdapters.get(workspace.uri); + if (testAdapter) { + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.pythonExecFactory, + ); + } else { + traceError('Unable to find test adapter for workspace.'); + } + } else { + traceError('Unable to find workspace for given file'); + } } else { // else use OLD test discovery mechanism await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index c0e1a310ee4..39dc87ed12c 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -18,7 +18,14 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; -import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; +import { + MESSAGE_ON_TESTING_OUTPUT_MOVE, + createDiscoveryErrorPayload, + createEOTPayload, + createTestingDeferred, + fixLogLinesNoTrailing, +} from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -29,6 +36,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { @@ -46,7 +54,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { await deferredTillEOT.promise; - traceVerbose('deferredTill EOT resolved'); + traceVerbose(`deferredTill EOT resolved for ${uri.fsPath}`); disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -61,18 +69,26 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.testServer.getPort().toString(); + traceInfo( + `All environment variables set for pytest discovery for workspace ${uri.fsPath}: ${JSON.stringify( + mutableEnv, + )} \n`, + ); const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, outputChannel: this.outputChannel, + env: mutableEnv, }; // Create the Python environment in which to execute the command. @@ -83,28 +99,38 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); - traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); + traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')} for workspace ${uri.fsPath}.`); const deferredTillExecClose: Deferred = createTestingDeferred(); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. + result?.proc?.stdout?.on('data', (data) => { - spawnOptions.outputChannel?.append(data.toString()); + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions.outputChannel?.append(data.toString()); + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.on('exit', (code, signal) => { + this.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { - traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}.`, + ); } }); result?.proc?.on('close', (code, signal) => { - if (code !== 0) { + // pytest exits with code of 5 when 0 tests are found- this is not a failure for discovery. + if (code !== 0 && code !== 5) { traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}. Creating and sending error discovery payload`, ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this.testServer.triggerDiscoveryDataReceivedEvent({ diff --git a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 8020be17cf9..8995a182a77 100644 --- a/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -24,6 +24,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; import * as utils from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -31,6 +32,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} async runTests( @@ -46,11 +48,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const deferredTillEOT: Deferred = utils.createTestingDeferred(); const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + runInstance?.token.isCancellationRequested; if (runInstance) { const eParsed = JSON.parse(e.data); this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); } else { - traceError('No run instance found, cannot resolve execution.'); + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }); const disposeDataReceiver = function (testServer: ITestServer) { @@ -59,7 +62,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { dataReceivedDisposable.dispose(); }; runInstance?.token.onCancellationRequested(() => { - traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + traceInfo(`Test run cancelled, resolving 'till EOT' deferred for ${uri.fsPath}.`); deferredTillEOT.resolve(); }); @@ -105,20 +108,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; - const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { - cwd, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - outputChannel: this.outputChannel, - stdinStr: testIds.toString(), + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.testServer.getPort().toString(); // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { @@ -141,9 +140,22 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.push('--capture', 'no'); } + // add port with run test ids to env vars const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); - if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + mutableEnv.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + traceInfo( + `All environment variables set for pytest execution in ${uri.fsPath} workspace: \n ${JSON.stringify( + mutableEnv, + )}`, + ); + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + outputChannel: this.outputChannel, + stdinStr: testIds.toString(), + env: mutableEnv, + }; if (debugBool) { const pytestPort = this.testServer.getPort().toString(); @@ -157,7 +169,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { pytestUUID, runTestIdsPort: pytestRunTestIdsPort.toString(), }; - traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); + traceInfo( + `Running DEBUG pytest with arguments: ${testArgs.join(' ')} for workspace ${uri.fsPath} \r\n`, + ); await debugLauncher!.launchDebugger(launchOptions, () => { deferredTillEOT?.resolve(); }); @@ -167,12 +181,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; - traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { - traceInfo('Test run cancelled, killing pytest subprocess.'); + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. if (resultProc) { resultProc?.kill(); @@ -186,15 +200,23 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. result?.proc?.stdout?.on('data', (data) => { - this.outputChannel?.append(data.toString()); + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(out); + this.outputChannel?.append(out); }); result?.proc?.stderr?.on('data', (data) => { - this.outputChannel?.append(data.toString()); + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(out); + this.outputChannel?.append(out); }); result?.proc?.on('exit', (code, signal) => { + this.outputChannel?.append(utils.MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0 && testIds) { - traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); } }); @@ -204,7 +226,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // if the child process exited with a non-zero exit code, then we need to send the error payload. if (code !== 0 && testIds) { traceError( - `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating and sending error execution payload \n`, ); this.testServer.triggerRunDataReceivedEvent({ uuid, @@ -223,7 +245,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { await deferredTillExecClose?.promise; } } catch (ex) { - traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } diff --git a/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 440df4f94dc..75e29afc971 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -15,6 +15,7 @@ import { TestDiscoveryCommand, } from '../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -25,13 +26,17 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} public async discoverTests(uri: Uri): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - + let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (env === undefined) { + env = {} as EnvironmentVariables; + } const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); @@ -52,7 +57,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { dataReceivedDisposable.dispose(); }; - await this.callSendCommand(options, () => { + await this.callSendCommand(options, env, () => { disposeDataReceiver?.(this.testServer); }); await deferredTillEOT.promise; @@ -66,8 +71,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, [], callback); + private async callSendCommand( + options: TestCommandOptions, + env: EnvironmentVariables, + callback: () => void, + ): Promise { + await this.testServer.sendCommand(options, env, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts b/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9da0872ef60..d90581a9311 100644 --- a/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -17,6 +17,7 @@ import { } from '../common/types'; import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -28,6 +29,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} public async runTests( @@ -78,6 +80,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildExecutionCommand(unittestArgs); + let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (env === undefined) { + env = {} as EnvironmentVariables; + } const options: TestCommandOptions = { workspaceFolder: uri, @@ -92,7 +98,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { + await this.testServer.sendCommand(options, env, runTestIdsPort.toString(), runInstance, testIds, () => { deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted diff --git a/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts b/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts index f3ea0b9f619..35a5b7a2441 100644 --- a/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts +++ b/extensions/positron-python/src/client/testing/testController/workspaceTestAdapter.ts @@ -143,7 +143,7 @@ export class WorkspaceTestAdapter { cancel = token?.isCancellationRequested ? Testing.cancelPytestDiscovery : Testing.errorPytestDiscovery; } - traceError(`${cancel}\r\n`, ex); + traceError(`${cancel} for workspace: ${this.workspaceUri} \r\n`, ex); // Report also on the test view. const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); diff --git a/extensions/positron-python/src/test/.vscode/settings.json b/extensions/positron-python/src/test/.vscode/settings.json index ef9292849a9..faeb48ffa29 100644 --- a/extensions/positron-python/src/test/.vscode/settings.json +++ b/extensions/positron-python/src/test/.vscode/settings.json @@ -3,7 +3,6 @@ "python.linting.flake8Enabled": false, "python.testing.pytestArgs": [], "python.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], - "python.sortImports.args": [], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.linting.pycodestyleEnabled": false, @@ -12,7 +11,6 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.linting.banditEnabled": false, - "python.formatting.provider": "yapf", // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. "python.languageServer": "Jedi", "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe" diff --git a/extensions/positron-python/src/test/common.ts b/extensions/positron-python/src/test/common.ts index 95345f91e5e..2ef366a3a47 100644 --- a/extensions/positron-python/src/test/common.ts +++ b/extensions/positron-python/src/test/common.ts @@ -40,24 +40,12 @@ export enum OSType { export type PythonSettingKeys = | 'defaultInterpreterPath' | 'languageServer' - | 'linting.lintOnSave' - | 'linting.enabled' - | 'linting.pylintEnabled' - | 'linting.flake8Enabled' - | 'linting.pycodestyleEnabled' - | 'linting.pylamaEnabled' - | 'linting.prospectorEnabled' - | 'linting.pydocstyleEnabled' - | 'linting.mypyEnabled' - | 'linting.banditEnabled' | 'testing.pytestArgs' | 'testing.unittestArgs' | 'formatting.provider' - | 'sortImports.args' | 'testing.pytestEnabled' | 'testing.unittestEnabled' | 'envFile' - | 'linting.ignorePatterns' | 'terminal.activateEnvironment'; async function disposePythonSettings() { diff --git a/extensions/positron-python/src/test/common/application/progressService.unit.test.ts b/extensions/positron-python/src/test/common/application/progressService.unit.test.ts new file mode 100644 index 00000000000..b9c49ccb406 --- /dev/null +++ b/extensions/positron-python/src/test/common/application/progressService.unit.test.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { CancellationToken, Progress, ProgressLocation, ProgressOptions } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { ProgressService } from '../../../client/common/application/progressService'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { createDeferred, createDeferredFromPromise, Deferred, sleep } from '../../../client/common/utils/async'; + +type ProgressTask = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, +) => Thenable; + +suite('Progress Service', () => { + let refreshDeferred: Deferred; + let shell: ApplicationShell; + let progressService: ProgressService; + setup(() => { + refreshDeferred = createDeferred(); + shell = mock(); + progressService = new ProgressService(instance(shell)); + }); + teardown(() => { + refreshDeferred.resolve(); + }); + test('Display discovering message when refreshing interpreters for the first time', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const expectedOptions = { title: 'message', location: ProgressLocation.Window }; + + progressService.showProgress(expectedOptions); + + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + assert.deepEqual(options, expectedOptions); + }); + + test('Progress message is hidden when loading has completed', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const options = { title: 'message', location: ProgressLocation.Window }; + progressService.showProgress(options); + + const callback = capture(shell.withProgress as never).last()[1] as ProgressTask; + const promise = callback(undefined as never, undefined as never); + const deferred = createDeferredFromPromise(promise as Promise); + await sleep(1); + expect(deferred.completed).to.be.equal(false, 'Progress disappeared before hiding it'); + progressService.hideProgress(); + await sleep(1); + expect(deferred.completed).to.be.equal(true, 'Progress did not disappear'); + }); +}); diff --git a/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts b/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts index eeaed6aa996..83b5b4a3d52 100644 --- a/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts +++ b/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts @@ -19,10 +19,7 @@ import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IAutoCompleteSettings, IExperiments, - IFormattingSettings, IInterpreterSettings, - ILintingSettings, - ISortImportSettings, ITerminalSettings, } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; @@ -117,9 +114,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); - config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); - config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); @@ -266,63 +260,4 @@ suite('Python Settings', async () => { test('Experiments (not enabled)', () => testExperiments(false)); test('Experiments (enabled)', () => testExperiments(true)); - - test('Formatter Paths and args', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: ['1', '2'], - autopep8Path: 'one', - blackArgs: ['3', '4'], - blackPath: 'two', - yapfArgs: ['5', '6'], - yapfPath: 'three', - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - expect((settings.formatting as any)[key]).to.be.deep.equal((expected.formatting as any)[key]); - } - config.verifyAll(); - }); - test('Formatter Paths (paths relative to home)', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: [], - autopep8Path: path.join('~', 'one'), - blackArgs: [], - blackPath: path.join('~', 'two'), - yapfArgs: [], - yapfPath: path.join('~', 'three'), - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - if (!key.endsWith('path')) { - continue; - } - - const expectedPath = untildify((expected.formatting as any)[key]); - - expect((settings.formatting as any)[key]).to.be.equal(expectedPath); - } - config.verifyAll(); - }); }); diff --git a/extensions/positron-python/src/test/common/experiments/service.unit.test.ts b/extensions/positron-python/src/test/common/experiments/service.unit.test.ts index 1d96f2e0bd7..ab05db6da5a 100644 --- a/extensions/positron-python/src/test/common/experiments/service.unit.test.ts +++ b/extensions/positron-python/src/test/common/experiments/service.unit.test.ts @@ -491,7 +491,10 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: ['foo'], optedOutFrom: ['bar'] }); + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['foo']), + optedOutFrom: JSON.stringify(['bar']), + }); }); test('Set telemetry properties to empty arrays if no experiments have been opted into or out from', async () => { @@ -523,7 +526,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); test('If the entered value for a setting contains "All", do not expand it to be a list of all experiments, and pass it as-is', async () => { @@ -555,7 +558,10 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[0]; - assert.deepStrictEqual(properties, { optedInto: ['All'], optedOutFrom: ['All'] }); + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['All']), + optedOutFrom: JSON.stringify(['All']), + }); }); // This is an unlikely scenario. @@ -577,7 +583,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); // This is also an unlikely scenario. @@ -608,7 +614,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); }); }); diff --git a/extensions/positron-python/src/test/common/installer.test.ts b/extensions/positron-python/src/test/common/installer.test.ts deleted file mode 100644 index 5c1842a2c97..00000000000 --- a/extensions/positron-python/src/test/common/installer.test.ts +++ /dev/null @@ -1,344 +0,0 @@ -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../client/activation/types'; -import { ActiveResourceService } from '../../client/common/application/activeResource'; -import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; -import { ClipboardService } from '../../client/common/application/clipboard'; -import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; -import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; -import { DebugService } from '../../client/common/application/debugService'; -import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { Extensions } from '../../client/common/application/extensions'; -import { - IActiveResourceService, - IApplicationEnvironment, - IApplicationShell, - IClipboard, - ICommandManager, - IDebugService, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { EditorUtils } from '../../client/common/editor'; -import { ExperimentService } from '../../client/common/experiments/service'; -import { InstallationChannelManager } from '../../client/common/installer/channelManager'; -import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { - IInstallationChannelManager, - IModuleInstaller, - IProductPathService, - IProductService, -} from '../../client/common/installer/types'; -import { InterpreterPathService } from '../../client/common/interpreterPathService'; -import { BrowserService } from '../../client/common/net/browser'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { ProcessLogger } from '../../client/common/process/logger'; -import { IProcessLogger, IProcessServiceFactory } from '../../client/common/process/types'; -import { TerminalActivator } from '../../client/common/terminal/activator'; -import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; -import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; -import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; -import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; -import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; -import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; -import { TerminalServiceFactory } from '../../client/common/terminal/factory'; -import { TerminalHelper } from '../../client/common/terminal/helper'; -import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; -import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; -import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; -import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; -import { - IShellDetector, - ITerminalActivationCommandProvider, - ITerminalActivationHandler, - ITerminalActivator, - ITerminalHelper, - ITerminalServiceFactory, - TerminalActivationProviders, -} from '../../client/common/terminal/types'; -import { - IBrowserService, - IConfigurationService, - ICurrentProcess, - IEditorUtils, - IExperimentService, - IExtensions, - IInstaller, - IInterpreterPathService, - IPathUtils, - IPersistentStateFactory, - IRandom, - IsWindows, - Product, - ProductType, -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { Random } from '../../client/common/utils/random'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IImportTracker } from '../../client/telemetry/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockModuleInstaller } from '../mocks/moduleInstaller'; -import { MockProcessService } from '../mocks/proc'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../initialize'; -import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; -import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; -import { - IPythonPathUpdaterServiceFactory, - IPythonPathUpdaterServiceManager, -} from '../../client/interpreter/configuration/types'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { getProductsForInstallerTests } from './productsToTest'; - -suite('Installer', () => { - let ioc: UnitTestIocContainer; - const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); - const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; - suiteSetup(initializeTest); - setup(async () => { - await initializeTest(); - await resetSettings(); - await initializeDI(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await resetSettings(); - }); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerUnitTestTypes(); - ioc.registerFileSystemTypes(); - ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); - ioc.registerInterpreterStorageTypes(); - - ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); - ioc.serviceManager.addSingleton(IPathUtils, PathUtils); - ioc.serviceManager.addSingleton(IProcessLogger, ProcessLogger); - ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); - ioc.serviceManager.addSingleton( - IInstallationChannelManager, - InstallationChannelManager, - ); - ioc.serviceManager.addSingletonInstance( - ICommandManager, - TypeMoq.Mock.ofType().object, - ); - - ioc.serviceManager.addSingletonInstance( - IApplicationShell, - TypeMoq.Mock.ofType().object, - ); - ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); - ioc.serviceManager.addSingleton(IWorkspaceService, WorkspaceService); - - await ioc.registerMockInterpreterTypes(); - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance(IsWindows, false); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - ioc.serviceManager.addSingleton( - IActivatedEnvironmentLaunch, - ActivatedEnvironmentLaunch, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory, - ); - ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); - ioc.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); - ioc.serviceManager.addSingleton(IExtensions, Extensions); - ioc.serviceManager.addSingleton(IRandom, Random); - ioc.serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); - ioc.serviceManager.addSingleton(IClipboard, ClipboardService); - ioc.serviceManager.addSingleton(IDocumentManager, DocumentManager); - ioc.serviceManager.addSingleton(IDebugService, DebugService); - ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); - ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); - ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); - ioc.serviceManager.addSingleton( - ITerminalActivationHandler, - PowershellTerminalActivationFailedHandler, - ); - ioc.serviceManager.addSingleton(IExperimentService, ExperimentService); - - ioc.serviceManager.addSingleton(ITerminalHelper, TerminalHelper); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - Bash, - TerminalActivationProviders.bashCShellFish, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - CommandPromptAndPowerShell, - TerminalActivationProviders.commandPromptAndPowerShell, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - Nushell, - TerminalActivationProviders.nushell, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - PyEnvActivationCommandProvider, - TerminalActivationProviders.pyenv, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - CondaActivationCommandProvider, - TerminalActivationProviders.conda, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - PipEnvActivationCommandProvider, - TerminalActivationProviders.pipenv, - ); - ioc.serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); - ioc.serviceManager.addSingleton(IImportTracker, ImportTracker); - ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); - ioc.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, SettingsShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - ReloadVSCodeCommandHandler, - ); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - ReportIssueCommandHandler, - ); - - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); - } - async function resetSettings() { - await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); - } - - async function testCheckingIfProductIsInstalled(product: Product) { - const installer = ioc.serviceContainer.get(IInstaller); - const processService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - const checkInstalledDef = createDeferred(); - processService.onExec((_file, args, _options, callback) => { - const moduleName = installer.translateProductToModuleName(product); - if (args.length > 1 && args[0] === '-c' && args[1] === `import ${moduleName}`) { - checkInstalledDef.resolve(true); - } - callback({ stdout: '' }); - }); - await installer.isInstalled(product, resource); - await checkInstalledDef.promise; - } - - getProductsForInstallerTests().forEach((prod) => { - test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { - if ( - new ProductService().getProductType(prod.value) === ProductType.DataScience || - new ProductService().getProductType(prod.value) === ProductType.Python - ) { - return this.skip(); - } - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('one', false), - ); - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('two', true), - ); - ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest) { - return undefined; - } - await testCheckingIfProductIsInstalled(prod.value); - - return undefined; - }).timeout(TEST_TIMEOUT * 3); - }); - - async function testInstallingProduct(product: Product) { - const installer = ioc.serviceContainer.get(IInstaller); - const checkInstalledDef = createDeferred(); - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const moduleInstallerOne = moduleInstallers.find((item) => item.displayName === 'two')!; - - moduleInstallerOne.on('installModule', (name: Product | string) => { - if (product === name) { - checkInstalledDef.resolve(); - } - }); - await installer.install(product); - await checkInstalledDef.promise; - } - - getProductsForInstallerTests().forEach((prod) => { - test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { - const productType = new ProductService().getProductType(prod.value); - if (productType === ProductType.DataScience || productType === ProductType.Python) { - return this.skip(); - } - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('one', false), - ); - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('two', true), - ); - ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest) { - return undefined; - } - await testInstallingProduct(prod.value); - - return undefined; - }).timeout(TEST_TIMEOUT * 3); - }); -}); diff --git a/extensions/positron-python/src/test/common/installer/channelManager.unit.test.ts b/extensions/positron-python/src/test/common/installer/channelManager.unit.test.ts index 319a9647fec..9789f9f1871 100644 --- a/extensions/positron-python/src/test/common/installer/channelManager.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/channelManager.unit.test.ts @@ -57,7 +57,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); expect(channel).to.equal(undefined, 'should be undefined'); assert.ok(showNoInstallersMessage.calledOnceWith(resource)); }); @@ -79,7 +79,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.equal(undefined, 'Channel should not be set'); @@ -107,7 +107,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should be set'); diff --git a/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts b/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts deleted file mode 100644 index b6738759f0d..00000000000 --- a/extensions/positron-python/src/test/common/installer/installer.invalidPath.unit.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductService } from '../../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../../client/common/installer/types'; -import { IPersistentState, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { getProductsForInstallerTests } from '../productsToTest'; - -use(chaiAsPromised); - -suite('Module Installer - Invalid Paths', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { - const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - - getProductsForInstallerTests().forEach((product) => { - let installer: ProductInstaller; - let serviceContainer: TypeMoq.IMock; - let app: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - let persistentState: TypeMoq.IMock; - - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - serviceContainer = TypeMoq.Mock.ofType(); - - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => new ProductService()); - app = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => app.object); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - - productPathService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) - .returns(() => productPathService.object); - - const interpreterService = TypeMoq.Mock.ofType(); - - const pythonInterpreter = TypeMoq.Mock.ofType(); - - pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonInterpreter.object)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - - persistentState = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) - .returns(() => persistentState.object); - - installer = new ProductInstaller(serviceContainer.object); - }); - - switch (product.value) { - case Product.unittest: { - return; - } - default: { - test(`Ensure invalid path message is ${isExecutableAModule ? 'not displayed' : 'displayed'} ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - // If the path to executable is a module, then we won't display error message indicating path is invalid. - - productPathService - .setup((p) => - p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource)), - ) - .returns(() => pathToExecutable) - .verifiable(TypeMoq.Times.atLeast(isExecutableAModule ? 0 : 1)); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => isExecutableAModule) - .verifiable(TypeMoq.Times.atLeastOnce()); - const anyParams = [0, 1, 2, 3, 4, 5].map(() => TypeMoq.It.isAny()); - app.setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), ...anyParams)) - .callback((message) => { - if (!isExecutableAModule) { - expect(message).contains(pathToExecutable); - } - }) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(1)); - const persistValue = TypeMoq.Mock.ofType>(); - persistValue.setup((pv) => pv.value).returns(() => false); - persistValue.setup((pv) => pv.updateValue(TypeMoq.It.isValue(true))); - persistentState - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistValue.object); - await installer.promptToInstall(product.value, resource); - productPathService.verifyAll(); - }); - } - } - }); - }); - }); -}); diff --git a/extensions/positron-python/src/test/common/installer/installer.unit.test.ts b/extensions/positron-python/src/test/common/installer/installer.unit.test.ts deleted file mode 100644 index 69a5f3678f6..00000000000 --- a/extensions/positron-python/src/test/common/installer/installer.unit.test.ts +++ /dev/null @@ -1,621 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -/* eslint-disable max-classes-per-file */ - -import { assert, expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductService } from '../../../client/common/installer/productService'; -import { - IInstallationChannelManager, - IModuleInstaller, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { - ExecutionResult, - IProcessService, - IProcessServiceFactory, - IPythonExecutionFactory, - IPythonExecutionService, -} from '../../../client/common/process/types'; -import { - IDisposableRegistry, - InstallerResponse, - IPersistentState, - IPersistentStateFactory, - Product, - ProductType, -} from '../../../client/common/types'; -import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { sleep } from '../../common'; -import { getProductsForInstallerTests } from '../productsToTest'; - -use(chaiAsPromised); - -suite('Module Installer only', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - getProductsForInstallerTests() - .concat([{ name: 'Unknown product', value: 404 }]) - - .forEach((product) => { - let disposables: Disposable[] = []; - let installer: ProductInstaller; - let installationChannel: TypeMoq.IMock; - let moduleInstaller: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let app: TypeMoq.IMock; - let promptDeferred: Deferred | undefined; - let workspaceService: TypeMoq.IMock; - let persistentStore: TypeMoq.IMock; - - let productPathService: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - const productService = new ProductService(); - - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - promptDeferred = createDeferred(); - serviceContainer = TypeMoq.Mock.ofType(); - - disposables = []; - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposables); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => productService); - installationChannel = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())) - .returns(() => installationChannel.object); - app = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => app.object); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - persistentStore = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) - .returns(() => persistentStore.object); - - moduleInstaller = TypeMoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleInstaller.setup((x: any) => x.then).returns(() => undefined); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(moduleInstaller.object)); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(moduleInstaller.object)); - - productPathService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) - .returns(() => productPathService.object); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => 'xyz'); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => true); - interpreterService = TypeMoq.Mock.ofType(); - const pythonInterpreter = TypeMoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonInterpreter.object)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - installer = new ProductInstaller(serviceContainer.object); - - return undefined; - }); - - teardown(() => { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - sinon.restore(); - return; - } - // This must be resolved, else all subsequent tests will fail (as this same promise will be used for other tests). - if (promptDeferred) { - promptDeferred.resolve(); - } - disposables.forEach((disposable) => { - if (disposable) { - disposable.dispose(); - } - }); - sinon.restore(); - }); - - switch (product.value) { - case 404 as Product: { - test(`If product type is not recognized, throw error (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - app.setup((a) => - a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ).verifiable(TypeMoq.Times.never()); - const getProductType = sinon.stub(ProductService.prototype, 'getProductType'); - - getProductType.returns('random' as ProductType); - const promise = installer.promptToInstall(product.value, resource); - await expect(promise).to.eventually.be.rejectedWith(`Unknown product ${product.value}`); - app.verifyAll(); - assert.ok(getProductType.calledOnce); - }); - return; - } - case Product.unittest: { - test(`Ensure resource info is passed into the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const response = await installer.install(product.value, resource); - expect(response).to.be.equal(InstallerResponse.Installed); - }); - test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const response = await installer.install(product.value, resource); - expect(response).to.be.equal(InstallerResponse.Installed); - }); - break; - } - - default: - test(`Ensure the prompt is displayed only once, until the prompt is closed, ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 5 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => promptDeferred!.promise) - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - // Display first prompt. - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - - // Display a few more prompts. - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - test(`Ensure the prompt is displayed again when previous prompt has been closed, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 3 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(3)); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - - if (product.value === Product.pylint) { - test(`Ensure the install prompt is not displayed when the user requests it not be shown again, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 2 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue("Don't show again"), - ), - ) - .returns(async () => "Don't show again") - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType>(); - let mockPersistVal = false; - persistVal.setup((p) => p.value).returns(() => mockPersistVal); - persistVal - .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }) - .verifiable(TypeMoq.Times.once()); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object) - .verifiable(TypeMoq.Times.exactly(3)); - - // Display first prompt. - const initialResponse = await installer.promptToInstall(product.value, resource); - - // Display a second prompt. - const secondResponse = await installer.promptToInstall(product.value, resource); - - expect(initialResponse).to.be.equal(InstallerResponse.Ignore); - expect(secondResponse).to.be.equal(InstallerResponse.Ignore); - - app.verifyAll(); - workspaceService.verifyAll(); - persistentStore.verifyAll(); - persistVal.verifyAll(); - }); - } else if (productService.getProductType(product.value) === ProductType.Linter) { - test(`Ensure the 'do not show again' prompt isn't shown for non-pylint linters, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - ), - ) - .returns(async () => undefined) - .verifiable(TypeMoq.Times.once()); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue("Don't show again"), - ), - ) - .returns(async () => undefined) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType>(); - let mockPersistVal = false; - persistVal.setup((p) => p.value).returns(() => mockPersistVal); - persistVal - .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - // Display the prompt. - await installer.promptToInstall(product.value, resource); - - // we're just ensuring the 'disable pylint' prompt never appears... - app.verifyAll(); - }); - } - - test(`Ensure resource info is passed into the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - - try { - await installer.install(product.value, resource); - } catch (ex) { - moduleInstaller.verify( - (m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - TypeMoq.Times.once(), - ); - } - }); - - test(`Return InstallerResponse.Ignore for the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - }) if installation channel is not defined`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - installationChannel.reset(); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - try { - const response = await installer.install(product.value, resource); - expect(response).to.equal(InstallerResponse.Ignore); - } catch (ex) { - assert(false, `Should not throw errors, ${ex}`); - } - }); - test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - - try { - await installer.install(product.value, resource); - } catch (ex) { - moduleInstaller.verify( - (m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - TypeMoq.Times.once(), - ); - } - }); - } - // Test isInstalled() - if (product.value === Product.unittest) { - test(`Method isInstalled() returns true for module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const result = await installer.isInstalled(product.value, resource); - expect(result).to.equal(true, 'Should be true'); - }); - } else { - test(`Method isInstalled() returns true if module is installed for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const pythonExecutionFactory = TypeMoq.Mock.ofType(); - const pythonExecutionService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) - .returns(() => pythonExecutionFactory.object); - pythonExecutionFactory - .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - pythonExecutionService - .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(true, 'Should be true'); - pythonExecutionService.verifyAll(); - }); - test(`Method isInstalled() returns false if module is not installed for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const pythonExecutionFactory = TypeMoq.Mock.ofType(); - const pythonExecutionService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) - .returns(() => pythonExecutionFactory.object); - pythonExecutionFactory - .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - pythonExecutionService - .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(false, 'Should be false'); - - pythonExecutionService.verifyAll(); - }); - test(`Method isInstalled() returns true if running 'path/to/module_executable --version' succeeds for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const processServiceFactory = TypeMoq.Mock.ofType(); - const processService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(IProcessServiceFactory)) - .returns(() => processServiceFactory.object); - processServiceFactory - .setup((p) => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - const executionResult: ExecutionResult = { - stdout: 'output', - }; - processService - .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(executionResult)) - .verifiable(TypeMoq.Times.once()); - - productPathService.reset(); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => false); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(true, 'Should be true'); - - processService.verifyAll(); - }); - test(`Method isInstalled() returns false if running 'path/to/module_executable --version' fails for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const processServiceFactory = TypeMoq.Mock.ofType(); - const processService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(IProcessServiceFactory)) - .returns(() => processServiceFactory.object); - processServiceFactory - .setup((p) => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - processService - .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.reject(new Error('Kaboom'))) - .verifiable(TypeMoq.Times.once()); - - productPathService.reset(); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => false); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(false, 'Should be false'); - - processService.verifyAll(); - }); - } - - // Test promptToInstall() when no interpreter is selected - test(`If no interpreter is selected, promptToInstall() doesn't prompt for product ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.never()); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - interpreterService.reset(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - interpreterService.verifyAll(); - workspaceService.verifyAll(); - }); - }); - }); -}); diff --git a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts index 01ac0e31555..3df64ceb2de 100644 --- a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts @@ -322,110 +322,6 @@ suite('Module Installer', () => { terminalService.verifyAll(); } - if (product.value === Product.pylint) { - generatePythonInterpreterVersions().forEach((interpreterInfo) => { - const majorVersion = interpreterInfo.version - ? interpreterInfo.version.major - : 0; - if (majorVersion === 2) { - const testTitle = `Ensure install arg is \'pylint<2.0.0\' in ${ - interpreterInfo.version ? interpreterInfo.version.raw : '' - }`; - if (InstallerClass === PipInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const proxyArgs = - proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = [ - '-m', - 'pip', - ...proxyArgs, - 'install', - '-U', - '"pylint<2.0.0"', - ]; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); - } - if (InstallerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', '"pylint<2.0.0"', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (InstallerClass === CondaInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push( - condaEnvInfo.name.toCommandArgumentForPythonExt(), - ); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push( - condaEnvInfo.path.fileToCommandArgumentForPythonExt(), - ); - } - expectedArgs.push('"pylint<2.0.0"'); - expectedArgs.push('-y'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - } else { - const testTitle = `Ensure install arg is \'pylint\' in ${ - interpreterInfo.version ? interpreterInfo.version.raw : '' - }`; - if (InstallerClass === PipInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const proxyArgs = - proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = [ - '-m', - 'pip', - ...proxyArgs, - 'install', - '-U', - 'pylint', - ]; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); - } - if (InstallerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', 'pylint', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (InstallerClass === CondaInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push( - condaEnvInfo.name.toCommandArgumentForPythonExt(), - ); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push( - condaEnvInfo.path.fileToCommandArgumentForPythonExt(), - ); - } - expectedArgs.push('pylint'); - expectedArgs.push('-y'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - } - }); - return; - } - if (InstallerClass === TestModuleInstaller) { suite(`If interpreter type is Unknown (${product.name})`, async () => { test(`If 'python.globalModuleInstallation' is set to true and pythonPath directory is read only, do an elevated install`, async () => { @@ -692,21 +588,6 @@ suite('Module Installer', () => { }); }); -function generatePythonInterpreterVersions() { - const versions: SemVer[] = ['2.7.0-final', '3.4.0-final', '3.5.0-final', '3.6.0-final', '3.7.0-final'].map( - (ver) => new SemVer(ver), - ); - return versions.map((version) => { - const info = TypeMoq.Mock.ofType(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.envType).returns(() => EnvironmentType.VirtualEnv); - info.setup((t) => t.version).returns(() => version); - info.setup((t) => t.path).returns(() => pythonPath); - return info.object; - }); -} - function getModuleNamesForTesting(): { name: string; value: Product; moduleName: string }[] { return getNamesAndValues(Product) .map((product) => { diff --git a/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts b/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts deleted file mode 100644 index 0f627289da7..00000000000 --- a/extensions/positron-python/src/test/common/installer/productPath.unit.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { fail } from 'assert'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { - BaseProductPathsService, - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../../client/common/installer/productPath'; -import { ProductService } from '../../../client/common/installer/productService'; -import { IProductService } from '../../../client/common/installer/types'; -import { - IConfigurationService, - IFormattingSettings, - IInstaller, - IPythonSettings, - Product, - ProductType, -} from '../../../client/common/types'; -import { IFormatterHelper } from '../../../client/formatters/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; -import { ITestsHelper } from '../../../client/testing/common/types'; -import { ITestingSettings } from '../../../client/testing/configuration/types'; -import { getProductsForInstallerTests } from '../productsToTest'; - -use(chaiAsPromised); - -suite('Product Path', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - getProductsForInstallerTests().forEach((product) => { - class TestBaseProductPathsService extends BaseProductPathsService { - public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { - return ''; - } - } - let serviceContainer: TypeMoq.IMock; - let formattingSettings: TypeMoq.IMock; - let unitTestSettings: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let productInstaller: ProductInstaller; - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - serviceContainer = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - formattingSettings = TypeMoq.Mock.ofType(); - unitTestSettings = TypeMoq.Mock.ofType(); - - productInstaller = new ProductInstaller(serviceContainer.object); - const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.formatting).returns(() => formattingSettings.object); - pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); - configService - .setup((s) => s.getSettings(TypeMoq.It.isValue(resource))) - .returns(() => pythonSettings.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => productInstaller); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => new ProductService()); - }); - - suite('Method isExecutableAModule()', () => { - test('Returns true if User has customized the executable name', () => { - productInstaller.translateProductToModuleName = () => 'moduleName'; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'executableName'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(true, 'Should be true'); - }); - test('Returns false if User has customized the full path to executable', () => { - productInstaller.translateProductToModuleName = () => 'moduleName'; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'path/to/executable'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(false, 'Should be false'); - }); - test('Returns false if translating product to module name fails with error', () => { - productInstaller.translateProductToModuleName = () => { - return new Error('Kaboom') as any; - }; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'executableName'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(false, 'Should be false'); - }); - }); - const productType = new ProductService().getProductType(product.value); - switch (productType) { - case ProductType.Formatter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new FormatterProductPathService(serviceContainer.object); - const formatterHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) - .returns(() => formatterHelper.object); - formattingSettings - .setup((f) => f.autopep8Path) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - formatterHelper - .setup((f) => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - pathName: 'autopep8Path', - argsName: 'autopep8Args', - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - formattingSettings.verifyAll(); - formatterHelper.verifyAll(); - }); - break; - } - case ProductType.Linter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new LinterProductPathService(serviceContainer.object); - const linterManager = TypeMoq.Mock.ofType(); - const linterInfo = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => linterManager.object); - linterInfo - .setup((l) => l.pathName(TypeMoq.It.isValue(resource))) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.once()); - linterManager - .setup((l) => l.getLinterInfo(TypeMoq.It.isValue(product.value))) - .returns(() => linterInfo.object) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - linterInfo.verifyAll(); - linterManager.verifyAll(); - }); - break; - } - case ProductType.TestFramework: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper - .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: 'pytestPath', - }; - }) - .verifiable(TypeMoq.Times.once()); - unitTestSettings - .setup((u) => u.pytestPath) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - testHelper.verifyAll(); - unitTestSettings.verifyAll(); - }); - test(`Ensure module name is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper - .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: undefined, - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - const moduleName = productInstaller.translateProductToModuleName(product.value); - expect(value).to.be.equal(moduleName); - testHelper.verifyAll(); - }); - break; - } - default: { - test(`No tests for Product Path of this Product Type ${product.name}`, () => { - fail('No tests for Product Path of this Product Type'); - }); - } - } - }); - }); -}); diff --git a/extensions/positron-python/src/test/common/installer/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/common/installer/serviceRegistry.unit.test.ts index a23cff298d6..8a811ad7ac4 100644 --- a/extensions/positron-python/src/test/common/installer/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/serviceRegistry.unit.test.ts @@ -9,11 +9,7 @@ import { CondaInstaller } from '../../../client/common/installer/condaInstaller' import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; import { PipInstaller } from '../../../client/common/installer/pipInstaller'; import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../../client/common/installer/productPath'; +import { TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { registerTypes } from '../../../client/common/installer/serviceRegistry'; import { @@ -46,20 +42,6 @@ suite('Common installer Service Registry', () => { ), ).once(); verify(serviceManager.addSingleton(IProductService, ProductService)).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ), - ).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ), - ).once(); verify( serviceManager.addSingleton( IProductPathService, diff --git a/extensions/positron-python/src/test/common/moduleInstaller.test.ts b/extensions/positron-python/src/test/common/moduleInstaller.test.ts index a6b647ad181..6d1d153aba9 100644 --- a/extensions/positron-python/src/test/common/moduleInstaller.test.ts +++ b/extensions/positron-python/src/test/common/moduleInstaller.test.ts @@ -3,7 +3,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import { SemVer } from 'semver'; import { instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; +import { Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../client/activation/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; @@ -29,7 +29,6 @@ import { } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { EditorUtils } from '../../client/common/editor'; import { ExperimentService } from '../../client/common/experiments/service'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; @@ -73,7 +72,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -98,7 +96,7 @@ import { JupyterExtensionDependencyManager } from '../../client/jupyter/jupyterE import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; import { ImportTracker } from '../../client/telemetry/importTracker'; import { IImportTracker } from '../../client/telemetry/types'; -import { PYTHON_PATH, rootWorkspaceUri } from '../common'; +import { PYTHON_PATH } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; @@ -132,7 +130,6 @@ suite('Module Installer', () => { chaiShould(); await initializeDI(); await initializeTest(); - await resetSettings(); }); suiteTeardown(async () => { await closeActiveWindows(); @@ -146,8 +143,6 @@ suite('Module Installer', () => { ioc = new UnitTestIocContainer(); ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); @@ -208,7 +203,6 @@ suite('Module Installer', () => { JupyterExtensionDependencyManager, ); ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); ioc.serviceManager.addSingleton( ITerminalActivationHandler, @@ -267,15 +261,6 @@ suite('Module Installer', () => { DebugSessionTelemetry, ); } - async function resetSettings(): Promise { - const configService = ioc.serviceManager.get(IConfigurationService); - await configService.updateSetting( - 'linting.pylintEnabled', - true, - rootWorkspaceUri, - ConfigurationTarget.Workspace, - ); - } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance( IModuleInstaller, diff --git a/extensions/positron-python/src/test/common/platform/fs-temp.functional.test.ts b/extensions/positron-python/src/test/common/platform/fs-temp.functional.test.ts index 256d52a81cf..9fb4fe189b9 100644 --- a/extensions/positron-python/src/test/common/platform/fs-temp.functional.test.ts +++ b/extensions/positron-python/src/test/common/platform/fs-temp.functional.test.ts @@ -5,7 +5,7 @@ import { expect, use } from 'chai'; import * as fs from 'fs-extra'; import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; import { TemporaryFile } from '../../../client/common/platform/types'; -import { assertDoesNotExist, assertExists, FSFixture, WINDOWS } from './utils'; +import { assertDoesNotExist, assertExists, FSFixture } from './utils'; const assertArrays = require('chai-arrays'); use(require('chai-as-promised')); @@ -56,21 +56,6 @@ suite('FileSystem - TemporaryFileSystem', () => { expect(filename1).to.not.equal(filename2); }); - test('Ensure writing to a temp file is supported via file stream', async function () { - if (WINDOWS) { - this.skip(); - } - const tempfile = await createFile('.tmp'); - const stream = fs.createWriteStream(tempfile.filePath); - fix.addCleanup(() => stream.destroy()); - const data = '...'; - - stream.write(data, 'utf8'); - - const actual = await fs.readFile(tempfile.filePath, 'utf8'); - expect(actual).to.equal(data); - }); - test('Ensure chmod works against a temporary file', async () => { // Note that on Windows chmod is a noop. const tempfile = await createFile('.tmp'); diff --git a/extensions/positron-python/src/test/common/productsToTest.ts b/extensions/positron-python/src/test/common/productsToTest.ts deleted file mode 100644 index 7fc06863f67..00000000000 --- a/extensions/positron-python/src/test/common/productsToTest.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Product } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; - -export function getProductsForInstallerTests(): { name: string; value: Product }[] { - return getNamesAndValues(Product).filter( - (p) => - ![ - 'pylint', - 'flake8', - 'pycodestyle', - 'pylama', - 'prospector', - 'pydocstyle', - 'yapf', - 'autopep8', - 'mypy', - 'isort', - 'black', - 'bandit', - ].includes(p.name), - ); -} diff --git a/extensions/positron-python/src/test/common/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/common/serviceRegistry.unit.test.ts index 2964455ada3..8ba7b7faaa9 100644 --- a/extensions/positron-python/src/test/common/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/common/serviceRegistry.unit.test.ts @@ -28,7 +28,6 @@ import { import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { PipEnvExecutionPath } from '../../client/common/configuration/executionSettings/pipEnvExecution'; -import { EditorUtils } from '../../client/common/editor'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; @@ -63,7 +62,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExtensions, IInstaller, IInterpreterPathService, @@ -103,7 +101,6 @@ suite('Common - Service Registry', () => { [IApplicationEnvironment, ApplicationEnvironment], [ILanguageService, LanguageService], [IBrowserService, BrowserService], - [IEditorUtils, EditorUtils], [ITerminalActivator, TerminalActivator], [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], [ITerminalHelper, TerminalHelper], diff --git a/extensions/positron-python/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/extensions/positron-python/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index cabf293ba95..58ae464d011 100644 --- a/extensions/positron-python/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/extensions/positron-python/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -63,7 +63,8 @@ suite('Activation of Environments in Terminal', () => { await terminalSettings.update('integrated.defaultProfile.linux', 'bash', vscode.ConfigurationTarget.Global); }); - setup(async () => { + setup(async function () { + this.skip(); // https://github.com/microsoft/vscode-python/issues/22264 await initializeTest(); outputFile = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, diff --git a/extensions/positron-python/src/test/configuration/environmentTypeComparer.unit.test.ts b/extensions/positron-python/src/test/configuration/environmentTypeComparer.unit.test.ts index 62c5d2d66b9..b7a4b82dc94 100644 --- a/extensions/positron-python/src/test/configuration/environmentTypeComparer.unit.test.ts +++ b/extensions/positron-python/src/test/configuration/environmentTypeComparer.unit.test.ts @@ -11,6 +11,7 @@ import { getEnvLocationHeuristic, } from '../../client/interpreter/configuration/environmentTypeComparer'; import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { PythonEnvType } from '../../client/pythonEnvironments/base/info'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; suite('Environment sorting', () => { @@ -45,6 +46,7 @@ suite('Environment sorting', () => { title: 'Local virtual environment should come first', envA: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -58,11 +60,13 @@ suite('Environment sorting', () => { title: "Non-local virtual environment should not come first when there's a local env", envA: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join('path', 'to', 'other', 'workspace', '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -72,10 +76,12 @@ suite('Environment sorting', () => { title: "Conda environment should not come first when there's a local env", envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -85,11 +91,13 @@ suite('Environment sorting', () => { title: 'Conda base environment should come after any other conda env', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'base', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'random-name', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -99,6 +107,7 @@ suite('Environment sorting', () => { title: 'Pipenv environment should come before any other conda env', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -118,19 +127,21 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.Poetry, + type: PythonEnvType.Virtual, envName: 'poetry-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, expected: 1, }, { - title: 'Pyenv environment should not come first when there are global envs', + title: 'Pyenv interpreter should not come first when there are global envs', envA: { envType: EnvironmentType.Pyenv, version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, envName: 'pipenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -144,6 +155,7 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.Poetry, + type: PythonEnvType.Virtual, envName: 'poetry-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -157,11 +169,25 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.VirtualEnv, + type: PythonEnvType.Virtual, envName: 'virtualenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, expected: 1, }, + { + title: + 'Microsoft Store interpreter should not come first when there are global interpreters with higher version', + envA: { + envType: EnvironmentType.MicrosoftStore, + version: { major: 3, minor: 10, patch: 2, raw: '3.10.2' }, + } as PythonEnvironment, + envB: { + envType: EnvironmentType.Global, + version: { major: 3, minor: 11, patch: 2, raw: '3.11.2' }, + } as PythonEnvironment, + expected: 1, + }, { title: 'Unknown environment should not come first when there are global envs', envA: { @@ -170,6 +196,7 @@ suite('Environment sorting', () => { } as PythonEnvironment, envB: { envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, envName: 'pipenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -179,11 +206,13 @@ suite('Environment sorting', () => { title: 'If 2 environments are of the same type, the most recent Python version comes first', envA: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.old-venv'), version: { major: 3, minor: 7, patch: 5, raw: '3.7.5' }, } as PythonEnvironment, envB: { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 2, raw: '3.10.2' }, } as PythonEnvironment, @@ -194,11 +223,13 @@ suite('Environment sorting', () => { "If 2 global environments have the same Python version and there's a Conda one, the Conda env should not come first", envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Pipenv, + type: PythonEnvType.Virtual, envName: 'pipenv-env', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -209,11 +240,13 @@ suite('Environment sorting', () => { 'If 2 global environments are of the same type and have the same Python version, they should be sorted by name', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-foo', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, envB: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envName: 'conda-bar', version: { major: 3, minor: 10, patch: 2 }, } as PythonEnvironment, @@ -237,6 +270,7 @@ suite('Environment sorting', () => { title: 'Problematic environments should come last', envA: { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envPath: path.join(workspacePath, '.venv'), path: 'python', } as PythonEnvironment, diff --git a/extensions/positron-python/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts b/extensions/positron-python/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts index 32ba68e03c7..2ec20be6699 100644 --- a/extensions/positron-python/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts +++ b/extensions/positron-python/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts @@ -14,6 +14,7 @@ import { EnvironmentTypeComparer } from '../../../client/interpreter/configurati import { InterpreterSelector } from '../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; import { IInterpreterComparer, IInterpreterQuickPickItem } from '../../../client/interpreter/configuration/types'; import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { getOSType, OSType } from '../../common'; @@ -139,12 +140,14 @@ suite('Interpreters - selector', () => { envPath: path.join('path', 'to', 'another', 'workspace', '.venv'), path: path.join('path', 'to', 'another', 'workspace', '.venv', 'bin', 'python'), envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, }, { displayName: 'two', envPath: path.join(workspacePath, '.venv'), path: path.join(workspacePath, '.venv', 'bin', 'python'), envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, }, { displayName: 'three', @@ -158,6 +161,7 @@ suite('Interpreters - selector', () => { path: path.join('a', 'conda', 'environment'), envName: 'conda-env', envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, }, ].map((item) => ({ ...info, ...item })); diff --git a/extensions/positron-python/src/test/format/extension.format.test.ts b/extensions/positron-python/src/test/format/extension.format.test.ts deleted file mode 100644 index 40131be24ec..00000000000 --- a/extensions/positron-python/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { CancellationTokenSource, Position, Uri, window, workspace } from 'vscode'; -import { IProcessServiceFactory } from '../../client/common/process/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { isPythonVersionInProcess } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { MockProcessService } from '../mocks/proc'; -import { registerForIOC } from '../pythonEnvironments/legacyIOC'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { compareFiles } from '../textUtils'; - -const ch = window.createOutputChannel('Tests'); -const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); -const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const originalUnformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); - -const autoPep8FileToFormat = path.join(formatFilesPath, 'autoPep8FileToFormat.py'); -const autoPep8Formatted = path.join(formatFilesPath, 'autoPep8Formatted.py'); -const blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); -const blackFormatted = path.join(formatFilesPath, 'blackFormatted.py'); -const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); -const yapfFormatted = path.join(formatFilesPath, 'yapfFormatted.py'); - -let formattedYapf = ''; -let formattedBlack = ''; -let formattedAutoPep8 = ''; - -suite('Formatting - General', () => { - let ioc: UnitTestIocContainer; - - suiteSetup(async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - // Skipping one test in the file is resulting in the next one failing, so skipping the entire suiteuntil further investigation. - - return this.skip(); - await initialize(); - await initializeDI(); - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - fs.copySync(originalUnformattedFile, file, { overwrite: true }); - }); - formattedYapf = fs.readFileSync(yapfFormatted).toString(); - formattedAutoPep8 = fs.readFileSync(autoPep8Formatted).toString(); - formattedBlack = fs.readFileSync(blackFormatted).toString(); - }); - - async function formattingTestIsBlackSupported(): Promise { - const processService = await ioc.serviceContainer - .get(IProcessServiceFactory) - .create(Uri.file(workspaceRootPath)); - return !(await isPythonVersionInProcess(processService, '2', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5')); - } - - setup(async () => { - await initializeTest(); - await initializeDI(); - }); - suiteTeardown(async () => { - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - ch.dispose(); - await closeActiveWindows(); - }); - teardown(async () => { - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.registerFormatterTypes(); - ioc.registerInterpreterStorageTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - await ioc.registerMockInterpreterTypes(); - - await registerForIOC(ioc.serviceManager, ioc.serviceContainer); - } - - async function injectFormatOutput(outputFileName: string) { - const procService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--diff') >= 0) { - callback({ - out: fs.readFileSync(path.join(formatFilesPath, outputFileName), 'utf8'), - source: 'stdout', - }); - } - }); - } - - async function testFormatting( - formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, - formattedContents: string, - fileToFormat: string, - outputFileName: string, - ) { - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - const options = { - insertSpaces: textEditor.options.insertSpaces! as boolean, - tabSize: textEditor.options.tabSize! as number, - }; - - await injectFormatOutput(outputFileName); - - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - compareFiles(formattedContents, textEditor.document.getText()); - } - - test('AutoPep8', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - await testFormatting( - new AutoPep8Formatter(ioc.serviceContainer), - formattedAutoPep8, - autoPep8FileToFormat, - 'autopep8.output', - ); - }); - - test('Black', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - if (!(await formattingTestIsBlackSupported())) { - // Skip for versions of python below 3.6, as Black doesn't support them at all. - - return this.skip(); - } - await testFormatting( - new BlackFormatter(ioc.serviceContainer), - formattedBlack, - blackFileToFormat, - 'black.output', - ); - }); - test('Yapf', async () => - testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); - - test('Yapf on dirty file', async () => { - const sourceDir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); - const targetDir = path.join(__dirname, '..', 'pythonFiles', 'formatting'); - - const originalName = 'formatWhenDirty.py'; - const resultsName = 'formatWhenDirtyResult.py'; - const fileToFormat = path.join(targetDir, originalName); - const formattedFile = path.join(targetDir, resultsName); - - if (!fs.pathExistsSync(targetDir)) { - fs.mkdirpSync(targetDir); - } - fs.copySync(path.join(sourceDir, originalName), fileToFormat, { overwrite: true }); - fs.copySync(path.join(sourceDir, resultsName), formattedFile, { overwrite: true }); - - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - await textEditor.edit((builder) => { - // Make file dirty. Trailing blanks will be removed. - builder.insert(new Position(0, 0), '\n \n'); - }); - - const dir = path.dirname(fileToFormat); - const configFile = path.join(dir, '.style.yapf'); - try { - // Create yapf configuration file - const content = '[style]\nbased_on_style = pep8\nindent_width=5\n'; - fs.writeFileSync(configFile, content); - - const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: 1 }; - const formatter = new YapfFormatter(ioc.serviceContainer); - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - - const expected = fs.readFileSync(formattedFile).toString(); - const actual = textEditor.document.getText(); - compareFiles(expected, actual); - } finally { - if (fs.existsSync(configFile)) { - fs.unlinkSync(configFile); - } - } - }); -}); diff --git a/extensions/positron-python/src/test/format/format.helper.test.ts b/extensions/positron-python/src/test/format/format.helper.test.ts deleted file mode 100644 index 50000f1af86..00000000000 --- a/extensions/positron-python/src/test/format/format.helper.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as assert from 'assert'; -import * as TypeMoq from 'typemoq'; -import { IConfigurationService, IFormattingSettings, Product } from '../../client/common/types'; -import * as EnumEx from '../../client/common/utils/enum'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { FormatterId } from '../../client/formatters/types'; -import { getExtensionSettings } from '../extensionSettings'; -import { initialize } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Formatting - Helper', () => { - let ioc: UnitTestIocContainer; - let formatHelper: FormatterHelper; - - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - - const config = TypeMoq.Mock.ofType(); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - ioc.serviceManager.addSingletonInstance(IConfigurationService, config.object); - formatHelper = new FormatterHelper(ioc.serviceManager); - }); - - test('Ensure product is set in Execution Info', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - assert.strictEqual( - info.product, - formatter, - `Incorrect products for ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure executable is set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - const names = formatHelper.getSettingsPropertyNames(formatter); - const execPath = settings.formatting[names.pathName] as string; - - assert.strictEqual( - info.execPath, - execPath, - `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure arguments are set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - const customArgs = ['1', '2', '3']; - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const names = formatHelper.getSettingsPropertyNames(formatter); - const args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - const expectedArgs = args.concat(customArgs).join(','); - - assert.strictEqual( - expectedArgs.endsWith(customArgs.join(',')), - true, - `Incorrect custom arguments for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure correct setting names are returned', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter)!; - const settings = { - argsName: `${translatedId}Args` as keyof IFormattingSettings, - pathName: `${translatedId}Path` as keyof IFormattingSettings, - }; - - assert.deepEqual( - formatHelper.getSettingsPropertyNames(formatter), - settings, - `Incorrect settings for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure translation of ids works', async () => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter); - assert.strictEqual( - translatedId, - formatterMapping.get(formatter)!, - `Incorrect translation for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - EnumEx.getValues(Product).forEach((product) => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - if (formatterMapping.has(product)) { - return; - } - - test(`Ensure translation of ids throws exceptions for unknown formatters (${product})`, async () => { - assert.throws(() => formatHelper.translateToId(product)); - }); - }); -}); diff --git a/extensions/positron-python/src/test/format/formatter.unit.test.ts b/extensions/positron-python/src/test/format/formatter.unit.test.ts deleted file mode 100644 index 05970d0c71f..00000000000 --- a/extensions/positron-python/src/test/format/formatter.unit.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { anything, capture, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, FormattingOptions, TextDocument, Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { - ExecutionInfo, - IConfigurationService, - IDisposableRegistry, - IFormattingSettings, - ILogOutputChannel, - IPythonSettings, -} from '../../client/common/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BaseFormatter } from '../../client/formatters/baseFormatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { IFormatterHelper } from '../../client/formatters/types'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { ServiceContainer } from '../../client/ioc/container'; -import { IServiceContainer } from '../../client/ioc/types'; -import { noop } from '../core'; -import { MockOutputChannel } from '../mockClasses'; - -suite('Formatting - Test Arguments', () => { - let container: IServiceContainer; - let outputChannel: ILogOutputChannel; - let workspace: IWorkspaceService; - let settings: IPythonSettings; - const workspaceUri = Uri.file(__dirname); - let document: typemoq.IMock; - const docUri = Uri.file(__filename); - let pythonToolExecutionService: IPythonToolExecutionService; - const options: FormattingOptions = { insertSpaces: false, tabSize: 1 }; - const formattingSettingsWithPath: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: path.join('a', 'exe'), - blackArgs: ['1', '2'], - blackPath: path.join('a', 'exe'), - provider: '', - yapfArgs: ['1', '2'], - yapfPath: path.join('a', 'exe'), - }; - - const formattingSettingsWithModuleName: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: 'module_name', - blackArgs: ['1', '2'], - blackPath: 'module_name', - provider: '', - yapfArgs: ['1', '2'], - yapfPath: 'module_name', - }; - - setup(() => { - container = mock(ServiceContainer); - outputChannel = mock(MockOutputChannel); - workspace = mock(WorkspaceService); - settings = mock(PythonSettings); - document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => ''); - document.setup((doc) => doc.isDirty).returns(() => false); - document.setup((doc) => doc.fileName).returns(() => docUri.fsPath); - document.setup((doc) => doc.uri).returns(() => docUri); - pythonToolExecutionService = mock(PythonToolExecutionService); - - const configService = mock(ConfigurationService); - const formatterHelper = new FormatterHelper(instance(container)); - - const appShell = mock(ApplicationShell); - when(appShell.setStatusBarMessage(anything(), anything())).thenReturn({ dispose: noop }); - - when(configService.getSettings(anything())).thenReturn(instance(settings)); - when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get(ILogOutputChannel)).thenReturn(instance(outputChannel)); - when(container.get(IApplicationShell)).thenReturn(instance(appShell)); - when(container.get(IFormatterHelper)).thenReturn(formatterHelper); - when(container.get(IWorkspaceService)).thenReturn(instance(workspace)); - when(container.get(IConfigurationService)).thenReturn(instance(configService)); - when(container.get(IPythonToolExecutionService)).thenReturn( - instance(pythonToolExecutionService), - ); - when(container.get(IDisposableRegistry)).thenReturn([]); - }); - - async function setupFormatter( - formatter: BaseFormatter, - formattingSettings: IFormattingSettings, - ): Promise { - const { token } = new CancellationTokenSource(); - when(settings.formatting).thenReturn(formattingSettings); - when(pythonToolExecutionService.exec(anything(), anything(), anything())).thenResolve({ stdout: '' }); - - await formatter.formatDocument(document.object, options, token); - - const args = capture(pythonToolExecutionService.exec).first(); - return args[0]; - } - test('Ensure blackPath and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.blackPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure black modulename and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.blackPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.blackPath); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure autopep8path and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.autopep8Path); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure autpep8 modulename and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.autopep8Path); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.autopep8Path); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapfpath and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.yapfPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapf modulename and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.yapfPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.yapfPath); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); -}); diff --git a/extensions/positron-python/src/test/initialize.ts b/extensions/positron-python/src/test/initialize.ts index add1d862446..487860410bf 100644 --- a/extensions/positron-python/src/test/initialize.ts +++ b/extensions/positron-python/src/test/initialize.ts @@ -31,6 +31,10 @@ export async function initializePython() { export async function initialize(): Promise { await initializePython(); + + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('experiments.optInto', ['All'], vscode.ConfigurationTarget.Global); + await pythonConfig.update('experiments.optOutFrom', [], vscode.ConfigurationTarget.Global); const api = await activateExtension(); if (!IS_SMOKE_TEST) { // When running smoke tests, we won't have access to these. diff --git a/extensions/positron-python/src/test/install/channelManager.channels.test.ts b/extensions/positron-python/src/test/install/channelManager.channels.test.ts index 5e102a0a518..0d8190f046a 100644 --- a/extensions/positron-python/src/test/install/channelManager.channels.test.ts +++ b/extensions/positron-python/src/test/install/channelManager.channels.test.ts @@ -89,7 +89,7 @@ suite('Installation - installation channels', () => { installer2.setup((x) => x.displayName).returns(() => 'Name 2'); const cm = new InstallationChannelManager(serviceContainer); - await cm.getInstallationChannel(Product.pylint); + await cm.getInstallationChannel(Product.pytest); assert.notStrictEqual(items, undefined, 'showQuickPick not called'); assert.strictEqual(items!.length, 2, 'Incorrect number of installer shown'); diff --git a/extensions/positron-python/src/test/install/channelManager.messages.test.ts b/extensions/positron-python/src/test/install/channelManager.messages.test.ts index c21612e8f56..326ba1ad4bf 100644 --- a/extensions/positron-python/src/test/install/channelManager.messages.test.ts +++ b/extensions/positron-python/src/test/install/channelManager.messages.test.ts @@ -185,7 +185,7 @@ suite('Installation - channel messages', () => { if (methodType === 'showNoInstallersMessage') { await channels.showNoInstallersMessage(); } else { - await channels.getInstallationChannel(Product.pylint); + await channels.getInstallationChannel(Product.pytest); } await verify(message, url); } diff --git a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/extensions/positron-python/src/test/interpreters/activation/indicatorPrompt.unit.test.ts similarity index 94% rename from extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts rename to extensions/positron-python/src/test/interpreters/activation/indicatorPrompt.unit.test.ts index baa83c8b11c..2214057fc95 100644 --- a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/activation/indicatorPrompt.unit.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, Uri, l10n } from 'vscode'; +import { EventEmitter, Terminal, Uri } from 'vscode'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; import { IConfigurationService, @@ -13,29 +13,31 @@ import { IPersistentStateFactory, IPythonSettings, } from '../../../client/common/types'; -import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; -import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; -suite('Terminal Environment Variable Collection Prompt', () => { +suite('Terminal Activation Indicator Prompt', () => { let shell: IApplicationShell; let terminalManager: ITerminalManager; let experimentService: IExperimentService; let activeResourceService: IActiveResourceService; let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; let persistentStateFactory: IPersistentStateFactory; - let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; const envName = 'env'; - const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(`, ${l10n.t('i.e')} "(${envName})"`); + const type = PythonEnvType.Virtual; + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format('Python virtual', `"(${envName})"`); setup(async () => { shell = mock(); @@ -43,6 +45,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { interpreterService = mock(); when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ envName, + type, } as unknown) as PythonEnvironment); experimentService = mock(); activeResourceService = mock(); @@ -61,7 +64,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { ); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); - terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( instance(shell), instance(persistentStateFactory), instance(terminalManager), diff --git a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index e41d6ce4d53..88b9c978854 100644 --- a/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -32,7 +32,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; @@ -331,7 +331,7 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.clear()).once(); verify(collection.prepend('PATH', prependedPart, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); - assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); test('Prepend full PATH with separator otherwise', async () => { @@ -364,7 +364,7 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.clear()).once(); verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); - assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); test('Verify envs are not applied if env activation is disabled', async () => { diff --git a/extensions/positron-python/src/test/interpreters/autoSelection/index.unit.test.ts b/extensions/positron-python/src/test/interpreters/autoSelection/index.unit.test.ts index f9c57f86708..d96d590628e 100644 --- a/extensions/positron-python/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/autoSelection/index.unit.test.ts @@ -23,6 +23,7 @@ import { EnvironmentTypeComparer } from '../../../client/interpreter/configurati import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import * as Telemetry from '../../../client/telemetry'; import { EventName } from '../../../client/telemetry/constants'; @@ -150,6 +151,7 @@ suite('Interpreters - Auto Selection', () => { test('If there is a local environment select it', async () => { const localEnv = { envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 0 }, } as PythonEnvironment; @@ -157,6 +159,7 @@ suite('Interpreters - Auto Selection', () => { when(interpreterService.getInterpreters(resource)).thenCall((_) => [ { envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, envPath: path.join('some', 'conda', 'env'), version: { major: 3, minor: 7, patch: 2 }, } as PythonEnvironment, diff --git a/extensions/positron-python/src/test/linters/bandit.unit.test.ts b/extensions/positron-python/src/test/linters/bandit.unit.test.ts deleted file mode 100644 index 6a44158034b..00000000000 --- a/extensions/positron-python/src/test/linters/bandit.unit.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { parseLine } from '../../client/linters/baseLinter'; -import { BANDIT_REGEX } from '../../client/linters/bandit'; - -import { ILintMessage, LinterId } from '../../client/linters/types'; - -suite('Linting - Bandit', () => { - test('parsing new bandit with col', () => { - const newOutput = `\ -1,0,LOW,B404:Consider possible security implications associated with subprocess module. -19,4,HIGH,B602:subprocess call with shell=True identified, security issue. -`; - - const lines = newOutput.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [ - lines[0], - { - code: 'B404', - message: 'Consider possible security implications associated with subprocess module.', - column: 0, - line: 1, - type: 'LOW', - provider: 'bandit', - }, - ], - [ - lines[1], - { - code: 'B602', - message: 'subprocess call with shell=True identified, security issue.', - column: 3, - line: 19, - type: 'HIGH', - provider: 'bandit', - }, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, BANDIT_REGEX, LinterId.Bandit, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('parsing old bandit with no col', () => { - const newOutput = `\ -1,col,LOW,B404:Consider possible security implications associated with subprocess module. -19,col,HIGH,B602:subprocess call with shell=True identified, security issue. -`; - - const lines = newOutput.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [ - lines[0], - { - code: 'B404', - message: 'Consider possible security implications associated with subprocess module.', - column: 0, - line: 1, - type: 'LOW', - provider: 'bandit', - }, - ], - [ - lines[1], - { - code: 'B602', - message: 'subprocess call with shell=True identified, security issue.', - column: 0, - line: 19, - type: 'HIGH', - provider: 'bandit', - }, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, BANDIT_REGEX, LinterId.Bandit, 1); - - expect(msg).to.deep.equal(expected); - } - }); -}); diff --git a/extensions/positron-python/src/test/linters/common.ts b/extensions/positron-python/src/test/linters/common.ts deleted file mode 100644 index 3c8f72a8d71..00000000000 --- a/extensions/positron-python/src/test/linters/common.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-disable max-classes-per-file */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as os from 'os'; -import * as TypeMoq from 'typemoq'; -import { DiagnosticSeverity, TextDocument, Uri, WorkspaceFolder } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IPythonExecutionFactory, IPythonToolExecutionService } from '../../client/common/process/types'; -import { - Flake8CategorySeverity, - IConfigurationService, - IInstaller, - IMypyCategorySeverity, - ILogOutputChannel, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, - IPythonSettings, -} from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinter, ILinterManager, ILintMessage, LinterId } from '../../client/linters/types'; - -export function newMockDocument(filename: string): TypeMoq.IMock { - const uri = Uri.file(filename); - const doc = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - doc.setup((s) => s.uri).returns(() => uri); - return doc; -} - -export function linterMessageAsLine(msg: ILintMessage): string { - switch (msg.provider) { - case 'pydocstyle': { - return `:${msg.line} spam:${os.EOL}\t${msg.code}: ${msg.message}`; - } - default: { - return `${msg.line},${msg.column},${msg.type},${msg.code}:${msg.message}`; - } - } -} - -function pylintMessageAsString(msg: ILintMessage, trailingComma = true): string { - return ` { - "type": "${msg.type}", - "line": ${msg.line}, - "column": ${msg.column}, - "symbol": "${msg.code}", - "message": "${msg.message}", - "endLine": ${msg.endLine ?? null}, - "endColumn": ${msg.endColumn ?? null} - }${trailingComma ? ',' : ''}`; -} - -export function pylintLinterMessagesAsOutput(messages: ILintMessage[]): string { - const lines: string[] = ['[']; - if (messages) { - const pylintMessages = messages.slice(0, -1).map((msg) => pylintMessageAsString(msg, true)); - const lastMessage = pylintMessageAsString(messages[messages.length - 1], false); - - lines.push(...pylintMessages, lastMessage); - } - lines.push(']'); - return lines.join(os.EOL); -} - -export function getLinterID(product: Product): LinterId { - const linterID = LINTERID_BY_PRODUCT.get(product); - if (!linterID) { - throwUnknownProduct(product); - } - return linterID!; -} - -export function getProductName(product: Product, capitalize = true): string { - let prodName = ProductNames.get(product); - if (!prodName) { - prodName = Product[product]; - } - if (capitalize) { - return prodName.charAt(0).toUpperCase() + prodName.slice(1); - } - return prodName; -} - -export function throwUnknownProduct(product: Product): void { - throw Error(`unsupported product ${Product[product]} (${product})`); -} - -export class LintingSettings { - public enabled: boolean; - - public cwd?: string; - - public ignorePatterns: string[]; - - public prospectorEnabled: boolean; - - public prospectorArgs: string[]; - - public pylintEnabled: boolean; - - public pylintArgs: string[]; - - public pycodestyleEnabled: boolean; - - public pycodestyleArgs: string[]; - - public pylamaEnabled: boolean; - - public pylamaArgs: string[]; - - public flake8Enabled: boolean; - - public flake8Args: string[]; - - public pydocstyleEnabled: boolean; - - public pydocstyleArgs: string[]; - - public lintOnSave: boolean; - - public maxNumberOfProblems: number; - - public pylintCategorySeverity: IPylintCategorySeverity; - - public pycodestyleCategorySeverity: IPycodestyleCategorySeverity; - - public flake8CategorySeverity: Flake8CategorySeverity; - - public mypyCategorySeverity: IMypyCategorySeverity; - - public prospectorPath: string; - - public pylintPath: string; - - public pycodestylePath: string; - - public pylamaPath: string; - - public flake8Path: string; - - public pydocstylePath: string; - - public mypyEnabled: boolean; - - public mypyArgs: string[]; - - public mypyPath: string; - - public banditEnabled: boolean; - - public banditArgs: string[]; - - public banditPath: string; - - constructor() { - // mostly from configSettings.ts - - this.enabled = true; - this.cwd = undefined; - this.ignorePatterns = []; - this.lintOnSave = false; - this.maxNumberOfProblems = 100; - - this.flake8Enabled = false; - this.flake8Path = 'flake8'; - this.flake8Args = []; - this.flake8CategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - F: DiagnosticSeverity.Warning, - }; - - this.mypyEnabled = false; - this.mypyPath = 'mypy'; - this.mypyArgs = []; - this.mypyCategorySeverity = { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint, - }; - - this.banditEnabled = false; - this.banditPath = 'bandit'; - this.banditArgs = []; - - this.pycodestyleEnabled = false; - this.pycodestylePath = 'pycodestyle'; - this.pycodestyleArgs = []; - this.pycodestyleCategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - }; - - this.pylamaEnabled = false; - this.pylamaPath = 'pylama'; - this.pylamaArgs = []; - - this.prospectorEnabled = false; - this.prospectorPath = 'prospector'; - this.prospectorArgs = []; - - this.pydocstyleEnabled = false; - this.pydocstylePath = 'pydocstyle'; - this.pydocstyleArgs = []; - - this.pylintEnabled = false; - this.pylintPath = 'pylint'; - this.pylintArgs = []; - this.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }; - } -} - -export class BaseTestFixture { - public serviceContainer: TypeMoq.IMock; - - public linterManager: LinterManager; - - // services - public workspaceService: TypeMoq.IMock; - - public installer: TypeMoq.IMock; - - public appShell: TypeMoq.IMock; - - // config - public configService: TypeMoq.IMock; - - public pythonSettings: TypeMoq.IMock; - - public lintingSettings: LintingSettings; - - // data - public outputChannel: TypeMoq.IMock; - - // artifacts - public output: string; - - public logged: string[]; - - constructor( - platformService: IPlatformService, - filesystem: IFileSystem, - pythonToolExecService: IPythonToolExecutionService, - pythonExecFactory: IPythonExecutionFactory, - configService?: TypeMoq.IMock, - serviceContainer?: TypeMoq.IMock, - ignoreConfigUpdates = false, - public readonly workspaceDir = '.', - protected readonly printLogs = false, - ) { - this.serviceContainer = - serviceContainer || TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - // services - - this.workspaceService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.installer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.appShell = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => this.workspaceService.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => this.installer.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) - .returns(() => platformService); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonToolExecutionService), TypeMoq.It.isAny())) - .returns(() => pythonToolExecService); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory), TypeMoq.It.isAny())) - .returns(() => pythonExecFactory); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => this.appShell.object); - this.initServices(); - - // config - - this.configService = - configService || TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.pythonSettings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.lintingSettings = new LintingSettings(); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => this.configService.object); - this.configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => this.pythonSettings.object); - this.pythonSettings.setup((s) => s.linting).returns(() => this.lintingSettings); - this.initConfig(ignoreConfigUpdates); - - // data - - this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) - .returns(() => this.outputChannel.object); - this.initData(); - - // artifacts - - this.output = ''; - this.logged = []; - - // linting - - this.linterManager = new LinterManager(this.configService.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => this.linterManager); - } - - public async getLinter(product: Product, enabled = true): Promise { - const info = this.linterManager.getLinterInfo(product); - - // @ts-ignore We only do this during testing. - this.lintingSettings[info.enabledSettingName] = enabled; - - await this.linterManager.setActiveLintersAsync([product]); - await this.linterManager.enableLintingAsync(enabled); - return this.linterManager.createLinter(product, this.serviceContainer.object); - } - - public async getEnabledLinter(product: Product): Promise { - return this.getLinter(product, true); - } - - public async getDisabledLinter(product: Product): Promise { - return this.getLinter(product, false); - } - - // eslint-disable-next-line class-methods-use-this - protected newMockDocument(filename: string): TypeMoq.IMock { - return newMockDocument(filename); - } - - private initServices(): void { - const workspaceFolder = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - workspaceFolder.setup((f) => f.uri).returns(() => Uri.file(this.workspaceDir)); - this.workspaceService - .setup((s) => s.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder.object); - - this.appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - } - - private initConfig(ignoreUpdates = false): void { - this.configService - .setup((c) => - c.updateSetting(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ) - .callback((setting, value) => { - if (ignoreUpdates) { - return; - } - const prefix = 'linting.'; - if (setting.startsWith(prefix)) { - // @ts-ignore We only do this during testing. - this.lintingSettings[setting.substr(prefix.length)] = value; - } - }) - .returns(() => Promise.resolve(undefined)); - - this.pythonSettings.setup((s) => s.languageServer).returns(() => LanguageServerType.Jedi); - } - - private initData(): void { - this.outputChannel - .setup((o) => o.appendLine(TypeMoq.It.isAny())) - .callback((line) => { - if (this.output === '') { - this.output = line; - } else { - this.output = `${this.output}${os.EOL}${line}`; - } - }); - this.outputChannel - .setup((o) => o.append(TypeMoq.It.isAny())) - .callback((data) => { - this.output += data; - }); - this.outputChannel.setup((o) => o.show()); - } -} diff --git a/extensions/positron-python/src/test/linters/lint.args.test.ts b/extensions/positron-python/src/test/linters/lint.args.test.ts deleted file mode 100644 index 2c32a73052b..00000000000 --- a/extensions/positron-python/src/test/linters/lint.args.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { Container } from 'inversify'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, Uri, WorkspaceFolder } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import '../../client/common/extensions'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { - IConfigurationService, - IExtensions, - IInstaller, - ILintingSettings, - IPythonSettings, -} from '../../client/common/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { Bandit } from '../../client/linters/bandit'; -import { BaseLinter } from '../../client/linters/baseLinter'; -import { Flake8 } from '../../client/linters/flake8'; -import { LinterManager } from '../../client/linters/linterManager'; -import { MyPy } from '../../client/linters/mypy'; -import { Prospector } from '../../client/linters/prospector'; -import { Pycodestyle } from '../../client/linters/pycodestyle'; -import { PyDocStyle } from '../../client/linters/pydocstyle'; -import { PyLama } from '../../client/linters/pylama'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Linting - Arguments', () => { - [undefined, path.join('users', 'dev_user')].forEach((workspaceUri) => { - [ - Uri.file(path.join('users', 'dev_user', 'development path to', 'one.py')), - Uri.file(path.join('users', 'dev_user', 'development', 'one.py')), - ].forEach((fileUri) => { - suite( - `File path ${fileUri.fsPath.indexOf(' ') > 0 ? 'with' : 'without'} spaces and ${ - workspaceUri ? 'without' : 'with' - } a workspace`, - () => { - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let document: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let extensionsService: TypeMoq.IMock; - const cancellationToken = new CancellationTokenSource().token; - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - - const fs = TypeMoq.Mock.ofType(); - fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( - () => new Promise((resolve) => resolve(true)), - ); - fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns( - () => true, - ); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance( - IInterpreterService, - interpreterService.object, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup((x) => x.enabled).returns(() => true); - lintSettings.setup((x) => x.lintOnSave).returns(() => true); - lintSettings.setup((x) => x.cwd).returns(() => undefined); - - settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance( - IConfigurationService, - configService.object, - ); - - const workspaceFolder: WorkspaceFolder | undefined = workspaceUri - ? { uri: Uri.file(workspaceUri), index: 0, name: '' } - : undefined; - workspaceService = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder); - serviceManager.addSingletonInstance( - IWorkspaceService, - workspaceService.object, - ); - - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - - const platformService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - - extensionsService = TypeMoq.Mock.ofType(); - extensionsService.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => undefined); - serviceManager.addSingletonInstance(IExtensions, extensionsService.object); - - lm = new LinterManager(configService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - document = TypeMoq.Mock.ofType(); - }); - - async function testLinter(linter: BaseLinter, expectedArgs: string[]) { - document.setup((d) => d.uri).returns(() => fileUri); - - let invoked = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (linter as any).run = (args: string[]) => { - expect(args).to.deep.equal(expectedArgs); - invoked = true; - return Promise.resolve([]); - }; - await linter.lint(document.object, cancellationToken); - expect(invoked).to.be.equal(true, 'method not invoked'); - } - test('Flake8', async () => { - const linter = new Flake8(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pycodestyle', async () => { - const linter = new Pycodestyle(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Prospector', async () => { - const linter = new Prospector(serviceContainer); - const expectedPath = workspaceUri - ? fileUri.fsPath.substring(workspaceUri.length + 2) - : path.basename(fileUri.fsPath); - const expectedArgs = [expectedPath]; - await testLinter(linter, expectedArgs); - }); - test('Pylama', async () => { - const linter = new PyLama(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('MyPy', async () => { - const linter = new MyPy(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pydocstyle', async () => { - const linter = new PyDocStyle(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pylint', async () => { - const linter = new Pylint(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Bandit', async () => { - const linter = new Bandit(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - }, - ); - }); - }); -}); diff --git a/extensions/positron-python/src/test/linters/lint.functional.test.ts b/extensions/positron-python/src/test/linters/lint.functional.test.ts deleted file mode 100644 index 9887cbc5605..00000000000 --- a/extensions/positron-python/src/test/linters/lint.functional.test.ts +++ /dev/null @@ -1,889 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, TextLine, Uri } from 'vscode'; -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 { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { - IProcessLogger, - IPythonExecutionFactory, - IPythonToolExecutionService, -} from '../../client/common/process/types'; -import { - IConfigurationService, - IDisposableRegistry, - IInterpreterPathService, - IPersistentState, -} from '../../client/common/types'; -import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; -import { - IActivatedEnvironmentLaunch, - IComponentAdapter, - IInterpreterService, -} from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; -import { deleteFile, PYTHON_PATH } from '../common'; -import { BaseTestFixture, getLinterID, getProductName, newMockDocument, throwUnknownProduct } from './common'; -import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; -import { Conda } from '../../client/pythonEnvironments/common/environmentManagers/conda'; -import * as promptApis from '../../client/linters/prompts/common'; - -const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const workspaceUri = Uri.file(workspaceDir); -const pythonFilesDir = path.join(workspaceDir, 'pythonFiles', 'linting'); -const fileToLint = path.join(pythonFilesDir, 'file.py'); - -const linterConfigDirs = new Map([ - [LinterId.Flake8, path.join(pythonFilesDir, 'flake8config')], - [LinterId.PyCodeStyle, path.join(pythonFilesDir, 'pycodestyleconfig')], - [LinterId.PyDocStyle, path.join(pythonFilesDir, 'pydocstyleconfig27')], - [LinterId.PyLint, path.join(pythonFilesDir, 'pylintconfig')], -]); -const linterConfigRCFiles = new Map([ - [LinterId.PyLint, '.pylintrc'], - [LinterId.PyDocStyle, '.pydocstyle'], -]); - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { - line: 24, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 30, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 34, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 40, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 44, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 55, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 59, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 62, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling undefined-variable (E0602)', - provider: '', - type: 'warning', - }, - { - line: 70, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 84, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 87, - column: 0, - severity: LintMessageSeverity.Hint, - code: 'C0304', - message: 'Final newline missing', - provider: '', - type: 'warning', - }, - { - line: 11, - column: 20, - severity: LintMessageSeverity.Warning, - code: 'W0613', - message: "Unused argument 'arg'", - provider: '', - type: 'warning', - }, - { - line: 26, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blop' member", - provider: '', - type: 'warning', - }, - { - line: 36, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 46, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 61, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 72, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 75, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 77, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 83, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 0, - line: 1, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 0, - line: 5, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D102', - severity: LintMessageSeverity.Information, - message: 'Missing docstring in public method', - column: 4, - line: 8, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D401', - severity: LintMessageSeverity.Information, - message: "First line should be in imperative mood ('thi', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('This', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('And', not 'and')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, -]; - -const filteredFlake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: '', - }, -]; -const filteredPycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: '', - }, -]; - -function getMessages(product: Product): ILintMessage[] { - switch (product) { - case Product.pylint: { - return pylintMessagesToBeReturned; - } - case Product.flake8: { - return flake8MessagesToBeReturned; - } - case Product.pycodestyle: { - return pycodestyleMessagesToBeReturned; - } - case Product.pydocstyle: { - return pydocstyleMessagesToBeReturned; - } - default: { - throwUnknownProduct(product); - return []; - } - } -} - -async function getInfoForConfig(product: Product) { - const prodID = getLinterID(product); - const dirname = linterConfigDirs.get(prodID); - assert.notStrictEqual(dirname, undefined, `tests not set up for ${Product[product]}`); - - const filename = path.join(dirname!, product === Product.pylint ? 'file2.py' : 'file.py'); - let messagesToBeReceived: ILintMessage[] = []; - switch (product) { - case Product.flake8: { - messagesToBeReceived = filteredFlake8MessagesToBeReturned; - break; - } - case Product.pycodestyle: { - messagesToBeReceived = filteredPycodestyleMessagesToBeReturned; - break; - } - default: { - break; - } - } - const basename = linterConfigRCFiles.get(prodID); - return { - filename, - messagesToBeReceived, - origRCFile: basename ? path.join(dirname!, basename) : '', - }; -} - -class TestFixture extends BaseTestFixture { - constructor(printLogs = false) { - const serviceContainer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const configService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const componentAdapter = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - componentAdapter - .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - - const filesystem = new FileSystem(); - processLogger - .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => { - /** No body */ - }); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IProcessLogger), TypeMoq.It.isAny())) - .returns(() => processLogger.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IComponentAdapter), TypeMoq.It.isAny())) - .returns(() => componentAdapter.object); - const activatedEnvironmentLaunch = TypeMoq.Mock.ofType(); - activatedEnvironmentLaunch - .setup((a) => a.selectIfLaunchedViaActivatedEnv()) - .returns(() => Promise.resolve(undefined)); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IActivatedEnvironmentLaunch), TypeMoq.It.isAny())) - .returns(() => activatedEnvironmentLaunch.object); - const platformService = new PlatformService(); - - super( - platformService, - filesystem, - TestFixture.newPythonToolExecService(serviceContainer.object), - TestFixture.newPythonExecFactory(serviceContainer, configService.object), - configService, - serviceContainer, - false, - workspaceDir, - printLogs, - ); - - this.pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); - } - - private static newPythonToolExecService(serviceContainer: IServiceContainer): IPythonToolExecutionService { - // We do not worry about the IProcessServiceFactory possibly - // needed by PythonToolExecutionService. - return new PythonToolExecutionService(serviceContainer); - } - - private static newPythonExecFactory( - serviceContainer: TypeMoq.IMock, - configService: IConfigurationService, - ): IPythonExecutionFactory { - const envVarsService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - envVarsService - .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(process.env)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) - .returns(() => envVarsService.object); - const disposableRegistry: IDisposableRegistry = []; - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposableRegistry); - - const envActivationService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - - const interpreterService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - - sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); - sinon.stub(Conda.prototype, 'getCondaVersion').resolves(undefined); - - const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - processLogger - .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => { - /** No body */ - }); - const procServiceFactory = new ProcessServiceFactory( - envVarsService.object, - processLogger.object, - disposableRegistry, - ); - const pyenvs: IComponentAdapter = mock(); - - const autoSelection = mock(); - const interpreterPathExpHelper = mock(); - when(interpreterPathExpHelper.get(anything())).thenReturn('selected interpreter path'); - - return new PythonExecutionFactory( - serviceContainer.object, - envActivationService.object, - procServiceFactory, - configService, - instance(pyenvs), - instance(autoSelection), - instance(interpreterPathExpHelper), - ); - } - - // eslint-disable-next-line class-methods-use-this - public makeDocument(filename: string): TextDocument { - const doc = newMockDocument(filename); - - doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((lno) => { - const lines = fs.readFileSync(filename).toString().split(os.EOL); - const textline = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - textline.setup((t) => t.text).returns(() => lines[lno]); - return textline.object; - }); - - return doc.object; - } -} - -suite('Linting Functional Tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let persistentState: TypeMoq.IMock>; - setup(() => { - isExtensionEnabledStub = sinon.stub(promptApis, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptApis, 'isExtensionDisabled'); - // For these tests we assume that linter extensions are not installed. - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - persistentState = TypeMoq.Mock.ofType>(); - persistentState.setup((p) => p.value).returns(() => true); - doNotShowPromptStateStub = sinon.stub(promptApis, 'doNotShowPromptState'); - doNotShowPromptStateStub.returns(persistentState.object); - }); - teardown(() => { - sinon.restore(); - }); - // These are integration tests that mock out everything except - // the filesystem and process execution. - - async function testLinterMessages( - fixture: TestFixture, - product: Product, - pythonFile: string, - messagesToBeReceived: ILintMessage[], - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter(product, fixture.serviceContainer.object); - - const messages = await linter.lint(doc, new CancellationTokenSource().token); - - if (messagesToBeReceived.length === 0) { - assert.strictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notStrictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(getProductName(product), async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - return this.skip(); - } - - const fixture = new TestFixture(); - const messagesToBeReturned = getMessages(product); - await testLinterMessages(fixture, product, fileToLint, messagesToBeReturned); - - return undefined; - }); - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`${getProductName(product)} with config in root`, async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - return this.skip(); - } - - const fixture = new TestFixture(); - const { filename, messagesToBeReceived, origRCFile } = await getInfoForConfig(product); - let rcfile = ''; - async function cleanUp() { - if (rcfile !== '') { - await deleteFile(rcfile); - } - } - if (origRCFile !== '') { - rcfile = path.join(workspaceUri.fsPath, path.basename(origRCFile)); - await fs.copy(origRCFile, rcfile); - } - - try { - await testLinterMessages(fixture, product, filename, messagesToBeReceived); - } finally { - await cleanUp(); - } - - return undefined; - }); - } - - async function testLinterMessageCount( - fixture: TestFixture, - product: Product, - pythonFile: string, - messageCountToBeReceived: number, - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter(product, fixture.serviceContainer.object); - - const messages = await linter.lint(doc, new CancellationTokenSource().token); - - assert.strictEqual( - messages.length, - messageCountToBeReceived, - 'Expected number of lint errors does not match lint error count', - ); - } - test('Three line output counted as one message', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors, - ); - }); - - test('Linters use config in cwd directory', async () => { - const maxErrors = 0; - const fixture = new TestFixture(); - fixture.lintingSettings.cwd = path.join(pythonFilesDir, 'pylintcwd'); - - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors, - ); - }); -}); diff --git a/extensions/positron-python/src/test/linters/lint.multiroot.test.ts b/extensions/positron-python/src/test/linters/lint.multiroot.test.ts deleted file mode 100644 index f89ee86c0b4..00000000000 --- a/extensions/positron-python/src/test/linters/lint.multiroot.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import { CancellationTokenSource, ConfigurationTarget, Uri, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, Product, ProductType } from '../../client/common/types'; -import { OSType } from '../../client/common/utils/platform'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { - IPythonPathUpdaterServiceManager, - IPythonPathUpdaterServiceFactory, -} from '../../client/interpreter/configuration/types'; -import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; -import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; -import { ILinter, ILinterManager } from '../../client/linters/types'; -import { isOs } from '../common'; -import { TEST_TIMEOUT } from '../constants'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); - -suite('Multiroot Linting', () => { - const pylintSetting = 'linting.pylintEnabled'; - const flake8Setting = 'linting.flake8Enabled'; - - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - if (!IS_MULTI_ROOT_TEST) { - this.skip(); - } - await initialize(); - await initializeDI(); - await initializeTest(); - }); - suiteTeardown(async () => { - await ioc?.dispose(); - await closeActiveWindows(); - PythonSettings.dispose(); - }); - teardown(async () => { - await closeActiveWindows(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerFileSystemTypes(); - await ioc.registerMockInterpreterTypes(); - ioc.serviceManager.addSingleton( - IActivatedEnvironmentLaunch, - ActivatedEnvironmentLaunch, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory, - ); - ioc.registerInterpreterStorageTypes(); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - } - - async function createLinter(product: Product): Promise { - const lm = ioc.serviceContainer.get(ILinterManager); - return lm.createLinter(product, ioc.serviceContainer); - } - async function testLinterInWorkspaceFolder( - product: Product, - workspaceFolderRelativePath: string, - mustHaveErrors: boolean, - ): Promise { - const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); - const cancelToken = new CancellationTokenSource(); - const document = await workspace.openTextDocument(fileToLint); - - const linter = await createLinter(product); - const messages = await linter.lint(document, cancelToken.token); - - const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; - assert.strictEqual(messages.length > 0, mustHaveErrors, errorMessage); - } - - test('Enabling Pylint in root and also in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.pylint, true, true, pylintSetting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.pylint, true, false, pylintSetting); - }).timeout(TEST_TIMEOUT * 2); - test('Disabling Pylint in root and enabling in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.pylint, false, true, pylintSetting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - - test('Enabling Flake8 in root and also in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.flake8, true, true, flake8Setting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.flake8, true, false, flake8Setting); - }).timeout(TEST_TIMEOUT * 2); - test('Disabling Flake8 in root and enabling in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.flake8, false, true, flake8Setting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - - async function runTest(product: Product, global: boolean, wks: boolean, setting: string): Promise { - const config = ioc.serviceContainer.get(IConfigurationService); - await config.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - Uri.file(multirootPath), - ConfigurationTarget.Global, - ); - await Promise.all([ - config.updateSetting(setting, global, Uri.file(multirootPath), ConfigurationTarget.Global), - config.updateSetting(setting, wks, Uri.file(multirootPath), ConfigurationTarget.Workspace), - ]); - await testLinterInWorkspaceFolder(product, 'workspace1', wks); - await Promise.all( - [ConfigurationTarget.Global, ConfigurationTarget.Workspace].map((configTarget) => - config.updateSetting(setting, undefined, Uri.file(multirootPath), configTarget), - ), - ); - } -}); diff --git a/extensions/positron-python/src/test/linters/lint.provider.test.ts b/extensions/positron-python/src/test/linters/lint.provider.test.ts deleted file mode 100644 index 760c2282ba0..00000000000 --- a/extensions/positron-python/src/test/linters/lint.provider.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Container } from 'inversify'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { IFileSystem } from '../../client/common/platform/types'; -import { - GLOBAL_MEMENTO, - IConfigurationService, - IInstaller, - ILintingSettings, - IMemento, - IPersistentStateFactory, - IPythonSettings, - Product, - Resource, - WORKSPACE_MEMENTO, -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockMemento } from '../mocks/mementos'; - -suite('Linting - Provider', () => { - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let emitter: vscode.EventEmitter; - let document: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let linterInstaller: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - - fs = TypeMoq.Mock.ofType(); - fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( - () => new Promise((resolve) => resolve(true)), - ); - fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup((x) => x.enabled).returns(() => true); - lintSettings.setup((x) => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - settings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - - configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - - appShell = TypeMoq.Mock.ofType(); - linterInstaller = TypeMoq.Mock.ofType(); - - workspaceService = TypeMoq.Mock.ofType(); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.object); - workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); - - serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - serviceManager.addSingletonInstance(IInstaller, linterInstaller.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); - serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance( - ICommandManager, - TypeMoq.Mock.ofType().object, - ); - lm = new LinterManager(configService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - emitter = new vscode.EventEmitter(); - document = TypeMoq.Mock.ofType(); - }); - - test('Lint on open file', async () => { - docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); - document.setup((x) => x.languageId).returns(() => 'python'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'auto'), TypeMoq.Times.once()); - }); - - test('Lint on save file', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); - document.setup((x) => x.languageId).returns(() => 'python'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.once()); - }); - - test('No lint on open other files', async () => { - docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); - document.setup((x) => x.languageId).returns(() => 'csharp'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('No lint on save other files', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); - document.setup((x) => x.languageId).returns(() => 'csharp'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('Lint on change interpreters', async () => { - const e = new vscode.EventEmitter(); - interpreterService.setup((x) => x.onDidChangeInterpreter).returns(() => e.event); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - e.fire(undefined); - engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Lint on save pylintrc', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('.pylintrc')); - - await lm.setActiveLintersAsync([Product.pylint]); - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - - const deferred = createDeferred(); - setTimeout(() => deferred.resolve(), 2000); - await deferred.promise; - engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Diagnostic cleared on file close', async () => testClearDiagnosticsOnClose(true)); - test('Diagnostic not cleared on file opened in another tab', async () => testClearDiagnosticsOnClose(false)); - - async function testClearDiagnosticsOnClose(closed: boolean) { - docManager.setup((x) => x.onDidCloseTextDocument).returns(() => emitter.event); - - const uri = vscode.Uri.file('test.py'); - document.setup((x) => x.uri).returns(() => uri); - document.setup((x) => x.isClosed).returns(() => closed); - - docManager.setup((x) => x.textDocuments).returns(() => (closed ? [] : [document.object])); - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - - emitter.fire(document.object); - const timesExpected = closed ? TypeMoq.Times.once() : TypeMoq.Times.never(); - engine.verify((x) => x.clearDiagnostics(TypeMoq.It.isAny()), timesExpected); - } -}); diff --git a/extensions/positron-python/src/test/linters/lint.test.ts b/extensions/positron-python/src/test/linters/lint.test.ts deleted file mode 100644 index d2eef3c9e32..00000000000 --- a/extensions/positron-python/src/test/linters/lint.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { ConfigurationTarget } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, ILintingSettings, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager } from '../../client/linters/types'; -import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Linting Settings', () => { - let ioc: UnitTestIocContainer; - let linterManager: ILinterManager; - let configService: IConfigurationService; - - suiteSetup(async () => { - await initialize(); - }); - setup(async () => { - await initializeDI(); - await initializeTest(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerPlatformTypes(); - configService = ioc.serviceContainer.get(IConfigurationService); - linterManager = new LinterManager(configService); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - } - - async function resetSettings(lintingEnabled = true) { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', lintingEnabled, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - const settingKey = `linting.${x.enabledSettingName}`; - await configService.updateSetting(settingKey, false, rootWorkspaceUri, target); - }); - } - - test('enable through manager (global)', async () => { - const settings = configService.getSettings(); - await resetSettings(false); - - await linterManager.enableLintingAsync(false); - assert.strictEqual(settings.linting.enabled, false, 'mismatch'); - - await linterManager.enableLintingAsync(true); - assert.strictEqual(settings.linting.enabled, true, 'mismatch'); - }); - - LINTERID_BY_PRODUCT.forEach((_, key) => { - const product = Product[key]; - - test(`enable through manager (${product})`, async () => { - const settings = configService.getSettings(); - await resetSettings(); - - const name = `${product}Enabled` as keyof ILintingSettings; - - assert.strictEqual(settings.linting[name], false, 'mismatch'); - - await linterManager.setActiveLintersAsync([key]); - - assert.strictEqual(settings.linting[name], true, 'mismatch'); - linterManager.getAllLinterInfos().forEach(async (x) => { - if (x.product !== key) { - assert.strictEqual( - settings.linting[x.enabledSettingName as keyof ILintingSettings], - false, - 'mismatch', - ); - } - }); - }); - }); -}); diff --git a/extensions/positron-python/src/test/linters/lint.unit.test.ts b/extensions/positron-python/src/test/linters/lint.unit.test.ts deleted file mode 100644 index 02bdd4c82c7..00000000000 --- a/extensions/positron-python/src/test/linters/lint.unit.test.ts +++ /dev/null @@ -1,854 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as os from 'os'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, TextLine } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - IPythonToolExecutionService, -} from '../../client/common/process/types'; -import { IPersistentState, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import * as promptApis from '../../client/linters/prompts/common'; -import { ILintMessage, LintMessageSeverity } from '../../client/linters/types'; -import { - BaseTestFixture, - getLinterID, - getProductName, - linterMessageAsLine, - pylintLinterMessagesAsOutput, - throwUnknownProduct, -} from './common'; - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { - line: 24, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 30, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 34, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 40, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 44, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 55, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 59, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 62, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling undefined-variable (E0602)', - provider: '', - type: 'warning', - }, - { - line: 70, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 84, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 87, - column: 0, - severity: LintMessageSeverity.Hint, - code: 'C0304', - message: 'Final newline missing', - provider: '', - type: 'warning', - }, - { - line: 11, - column: 20, - severity: LintMessageSeverity.Warning, - code: 'W0613', - message: "Unused argument 'arg'", - provider: '', - type: 'warning', - }, - { - line: 26, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blop' member", - provider: '', - type: 'warning', - }, - { - line: 36, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 46, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: undefined, - endColumn: undefined, - }, - { - line: 61, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 61, - endColumn: undefined, - }, - { - line: 72, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 72, - endColumn: 28, - }, - { - line: 75, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 75, - endColumn: 28, - }, - { - line: 77, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 77, - endColumn: 24, - }, - { - line: 83, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 83, - endColumn: 24, - }, -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 0, - line: 1, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 0, - line: 5, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D102', - severity: LintMessageSeverity.Information, - message: 'Missing docstring in public method', - column: 4, - line: 8, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D401', - severity: LintMessageSeverity.Information, - message: "First line should be in imperative mood ('thi', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('This', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('And', not 'and')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, -]; - -class TestFixture extends BaseTestFixture { - public platformService: TypeMoq.IMock; - - public filesystem: TypeMoq.IMock; - - public pythonToolExecService: TypeMoq.IMock; - - public pythonExecService: TypeMoq.IMock; - - public pythonExecFactory: TypeMoq.IMock; - - constructor(workspaceDir = '.', printLogs = false) { - const platformService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const filesystem = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const pythonToolExecService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - const pythonExecFactory = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - super( - platformService.object, - filesystem.object, - pythonToolExecService.object, - pythonExecFactory.object, - undefined, - undefined, - true, - workspaceDir, - printLogs, - ); - - this.platformService = platformService; - this.filesystem = filesystem; - this.pythonToolExecService = pythonToolExecService; - this.pythonExecService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.pythonExecFactory = pythonExecFactory; - - this.filesystem.setup((f) => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.pythonExecService.setup((s: any) => s.then).returns(() => undefined); - this.pythonExecService - .setup((s) => s.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - this.pythonExecFactory - .setup((f) => f.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(this.pythonExecService.object)); - } - - public makeDocument(product: Product, filename: string): TextDocument { - const doc = this.newMockDocument(filename); - if (product === Product.pydocstyle) { - const dummyLine = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - dummyLine.setup((d) => d.text).returns(() => ' ...'); - doc.setup((s) => s.lineAt(TypeMoq.It.isAny())).returns(() => dummyLine.object); - } - return doc.object; - } - - public setDefaultMessages(product: Product): ILintMessage[] { - let messages: ILintMessage[]; - switch (product) { - case Product.pylint: { - messages = pylintMessagesToBeReturned; - break; - } - case Product.flake8: { - messages = flake8MessagesToBeReturned; - break; - } - case Product.pycodestyle: { - messages = pycodestyleMessagesToBeReturned; - break; - } - case Product.pydocstyle: { - messages = pydocstyleMessagesToBeReturned; - break; - } - default: { - throwUnknownProduct(product); - return []; - } - } - this.setMessages(messages, product); - return messages; - } - - public setMessages(messages: ILintMessage[], product?: Product) { - if (messages.length === 0) { - this.setStdout(''); - return; - } - - if (product && getLinterID(product) === 'pylint') { - this.setStdout(pylintLinterMessagesAsOutput(messages)); - return; - } - const lines: string[] = []; - for (const msg of messages) { - if (msg.provider === '' && product) { - msg.provider = getLinterID(product); - } - const line = linterMessageAsLine(msg); - lines.push(line); - } - this.setStdout(lines.join(os.EOL) + os.EOL); - } - - public setStdout(stdout: string) { - this.pythonToolExecService - .setup((s) => s.execForLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout })); - } -} - -suite('Linting Scenarios', () => { - // Note that these aren't actually unit tests. Instead they are - // integration tests with heavy usage of mocks. - - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let persistentState: TypeMoq.IMock>; - setup(() => { - isExtensionEnabledStub = sinon.stub(promptApis, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptApis, 'isExtensionDisabled'); - // For these tests we assume that linter extensions are not installed. - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - persistentState = TypeMoq.Mock.ofType>(); - persistentState.setup((p) => p.value).returns(() => true); - doNotShowPromptStateStub = sinon.stub(promptApis, 'doNotShowPromptState'); - doNotShowPromptStateStub.returns(persistentState.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('No linting with PyLint (enabled) when disabled at top-level', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = false; - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`, - ); - }); - - test('No linting with Pylint disabled (and Flake8 enabled)', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = true; - fixture.lintingSettings.flake8Enabled = true; - fixture.setDefaultMessages(Product.pylint); - const linter = await fixture.getDisabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`, - ); - }); - - async function testEnablingDisablingOfLinter(fixture: TestFixture, product: Product, enabled: boolean) { - fixture.lintingSettings.enabled = true; - fixture.setDefaultMessages(product); - if (enabled) { - fixture.setDefaultMessages(product); - } - const linter = await fixture.getLinter(product, enabled); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - if (enabled) { - assert.notStrictEqual( - messages.length, - 0, - `Expected linter errors when linter is enabled, Output - ${fixture.output}`, - ); - } else { - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linter is disabled, Output - ${fixture.output}`, - ); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - for (const enabled of [false, true]) { - test(`${enabled ? 'Enable' : 'Disable'} ${getProductName(product)} and run linter`, async function () { - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - this.skip(); - } - - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, product, enabled); - }); - } - } - for (const useMinimal of [true, false]) { - for (const enabled of [true, false]) { - test(`PyLint ${enabled ? 'enabled' : 'disabled'} with${ - useMinimal ? '' : 'out' - } minimal checkers`, async () => { - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, Product.pylint, enabled); - }); - } - } - - async function testLinterMessages(fixture: TestFixture, product: Product) { - const messagesToBeReceived = fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - if (messagesToBeReceived.length === 0) { - assert.strictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notStrictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`Check ${getProductName(product)} messages`, async function () { - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - this.skip(); - } - - const fixture = new TestFixture(); - await testLinterMessages(fixture, product); - }); - } - - async function testLinterMessageCount(fixture: TestFixture, product: Product, messageCountToBeReceived: number) { - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - messageCountToBeReceived, - `Expected number of lint errors does not match lint error count, Output - ${fixture.output}`, - ); - } - test('Three line output counted as one message (Pylint)', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - - await testLinterMessageCount(fixture, Product.pylint, maxErrors); - }); -}); - -suite('Linting Products', () => { - const prodService = new ProductService(); - - test('All linting products are represented by linters', async () => { - const products = Object.keys(Product) - .filter((item) => Number.isNaN(Number(item))) - .map((key) => Product[Number(key)]); - - products.forEach((p) => { - const product = (p as unknown) as Product; - if (prodService.getProductType(product) === ProductType.Linter) { - const found = LINTERID_BY_PRODUCT.get(product); - assert.notStrictEqual(found, undefined, `did find linter ${Product[product]}`); - } - }); - }); - - test('All linters match linting products', async () => { - for (const product of LINTERID_BY_PRODUCT.keys()) { - const prodType = prodService.getProductType(product); - assert.notStrictEqual(prodType, undefined, `${Product[product]} is not not properly registered`); - assert.strictEqual(prodType, ProductType.Linter, `${Product[product]} is not a linter product`); - } - }); - - test('All linting product names match linter IDs', async () => { - for (const [product, linterID] of LINTERID_BY_PRODUCT) { - const prodName = ProductNames.get(product); - assert.strictEqual(prodName, linterID, 'product name does not match linter ID'); - } - }); -}); diff --git a/extensions/positron-python/src/test/linters/lintengine.test.ts b/extensions/positron-python/src/test/linters/lintengine.test.ts deleted file mode 100644 index 1bf77c502af..00000000000 --- a/extensions/positron-python/src/test/linters/lintengine.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as TypeMoq from 'typemoq'; -import { TextDocument, Uri } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PYTHON_LANGUAGE } from '../../client/common/constants'; -import '../../client/common/extensions'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, ILintingSettings, ILogOutputChannel, IPythonSettings } from '../../client/common/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { PythonEnvironment } from '../../client/pythonEnvironments/info'; -import { initialize } from '../initialize'; - -suite('Linting - LintingEngine', () => { - let serviceContainer: TypeMoq.IMock; - let lintManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lintSettings: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let lintingEngine: ILintingEngine; - - suiteSetup(initialize); - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType(); - - const docManager = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())) - .returns(() => docManager.object); - - const workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => fileSystem.object); - - lintSettings = TypeMoq.Mock.ofType(); - settings = TypeMoq.Mock.ofType(); - - const configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - configService.setup((x) => x.isTestExecution()).returns(() => true); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - - const outputChannel = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); - - lintManager = TypeMoq.Mock.ofType(); - lintManager.setup((x) => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => lintManager.object); - - lintingEngine = new LintingEngine(serviceContainer.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILintingEngine), TypeMoq.It.isAny())) - .returns(() => lintingEngine); - - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); - serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); - }); - - test('Ensure document.uri is passed into isLintingEnabled', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify((l) => l.isLintingEnabled(TypeMoq.It.isValue(doc.uri)), TypeMoq.Times.once()); - } - }); - test('Ensure document.uri is passed into createLinter', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify( - (l) => - l.createLinter( - TypeMoq.It.isAny(), - - TypeMoq.It.isAny(), - TypeMoq.It.isValue(doc.uri), - ), - TypeMoq.Times.atLeastOnce(), - ); - } - }); - - test('Verify files that match ignore pattern are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, true, ['a*.py']); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure non-Python files are not linted', async () => { - const doc = mockTextDocument('a.ts', 'typescript', true); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure files with git scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'git'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - test('Ensure files with showModifications scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'showModifications'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - test('Ensure files with svn scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'svn'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure non-existing files are not linted', async () => { - const doc = mockTextDocument('file.py', PYTHON_LANGUAGE, false, []); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - function mockTextDocument( - fileName: string, - language: string, - exists: boolean, - ignorePattern: string[] = [], - scheme?: string, - ): TextDocument { - fileSystem.setup((x) => x.fileExists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); - - lintSettings.setup((l) => l.ignorePatterns).returns(() => ignorePattern); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - - const doc = TypeMoq.Mock.ofType(); - if (scheme) { - doc.setup((d) => d.uri).returns(() => Uri.parse(`${scheme}:${fileName}`)); - } else { - doc.setup((d) => d.uri).returns(() => Uri.file(fileName)); - } - doc.setup((d) => d.fileName).returns(() => fileName); - doc.setup((d) => d.languageId).returns(() => language); - return doc.object; - } -}); diff --git a/extensions/positron-python/src/test/linters/linterManager.unit.test.ts b/extensions/positron-python/src/test/linters/linterManager.unit.test.ts deleted file mode 100644 index 42feb642ce8..00000000000 --- a/extensions/positron-python/src/test/linters/linterManager.unit.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { IConfigurationService, Product, ProductType } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterInfo } from '../../client/linters/linterInfo'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Manager', () => { - let linterManager: LinterManagerTest; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - class LinterManagerTest extends LinterManager { - // Override base class property to make it public. - public linters!: ILinterInfo[]; - } - setup(() => { - const svcContainer = mock(ServiceContainer); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - when(svcContainer.get(IConfigurationService)).thenReturn(instance(configService)); - when(svcContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - linterManager = new LinterManagerTest(instance(configService)); - }); - - test('Get all linters will return a list of all linters', () => { - const linters = linterManager.getAllLinterInfos(); - - expect(linters).to.be.lengthOf(8); - - const productService = new ProductService(); - const linterProducts = getNamesAndValues(Product) - .filter((product) => productService.getProductType(product.value) === ProductType.Linter) - .map((item) => ProductNames.get(item.value)); - expect(linters.map((item) => item.id).sort()).to.be.deep.equal(linterProducts.sort()); - }); - - test('Get linter info for non-linter product should throw an exception', () => { - const productService = new ProductService(); - getNamesAndValues(Product).forEach((prod) => { - if (productService.getProductType(prod.value) === ProductType.Linter) { - const info = linterManager.getLinterInfo(prod.value); - expect(info.id).to.equal(ProductNames.get(prod.value)); - expect(info).not.to.be.equal(undefined, 'should not be unedfined'); - } else { - expect(() => linterManager.getLinterInfo(prod.value)).to.throw(); - } - }); - }); - test('Pylint configuration file watch', async () => { - const pylint = linterManager.getLinterInfo(Product.pylint); - assert.strictEqual(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); - assert.notStrictEqual( - pylint.configFileNames.indexOf('pylintrc'), - -1, - 'Pylint configuration files miss pylintrc.', - ); - assert.notStrictEqual( - pylint.configFileNames.indexOf('.pylintrc'), - -1, - 'Pylint configuration files miss .pylintrc.', - ); - }); - - [undefined, Uri.parse('something')].forEach((resource) => { - const testResourceSuffix = `(${resource ? 'with a resource' : 'without a resource'})`; - [true, false].forEach((enabled) => { - const testSuffix = `(${enabled ? 'enable' : 'disable'}) & ${testResourceSuffix}`; - test(`Enable linting should update config ${testSuffix}`, async () => { - when(configService.updateSetting('linting.enabled', enabled, resource)).thenResolve(); - - await linterManager.enableLintingAsync(enabled, resource); - - verify(configService.updateSetting('linting.enabled', enabled, resource)).once(); - }); - }); - test(`getActiveLinters will check if linter is enabled and in silent mode ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.isEnabled(resource)).thenReturn(true); - - const linters = await linterManager.getActiveLinters(resource); - - verify(linterInfo.isEnabled(resource)).once(); - expect(linters[0]).to.deep.equal(instanceOfLinterInfo); - }); - - test(`setActiveLintersAsync with invalid products does nothing ${testResourceSuffix}`, async () => { - let getActiveLintersInvoked = false; - linterManager.getActiveLinters = async () => { - getActiveLintersInvoked = true; - return []; - }; - - await linterManager.setActiveLintersAsync([Product.pytest], resource); - - expect(getActiveLintersInvoked).to.be.equal(false, 'Should not be invoked'); - }); - test(`setActiveLintersAsync with single product will disable it then enable it ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.product).thenReturn(Product.flake8); - when(linterInfo.enableAsync(false, resource)).thenResolve(); - linterManager.getActiveLinters = () => Promise.resolve([instanceOfLinterInfo]); - linterManager.enableLintingAsync = () => Promise.resolve(); - - await linterManager.setActiveLintersAsync([Product.flake8], resource); - - verify(linterInfo.enableAsync(false, resource)).atLeast(1); - verify(linterInfo.enableAsync(true, resource)).atLeast(1); - }); - test(`setActiveLintersAsync with single product will disable all existing then enable the necessary two ${testResourceSuffix}`, async () => { - const linters = new Map(); - const linterInstances = new Map(); - linterManager.linters = []; - [Product.flake8, Product.mypy, Product.prospector, Product.bandit, Product.pydocstyle].forEach( - (product) => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters.push(instanceOfLinterInfo); - linters.set(product, linterInfo); - linterInstances.set(product, instanceOfLinterInfo); - when(linterInfo.product).thenReturn(product); - when(linterInfo.enableAsync(anything(), resource)).thenResolve(); - }, - ); - - linterManager.getActiveLinters = () => Promise.resolve(Array.from(linterInstances.values())); - linterManager.enableLintingAsync = () => Promise.resolve(); - - const lintersToEnable = [Product.flake8, Product.mypy, Product.pydocstyle]; - await linterManager.setActiveLintersAsync([Product.flake8, Product.mypy, Product.pydocstyle], resource); - - linters.forEach((item, product) => { - verify(item.enableAsync(false, resource)).atLeast(1); - if (lintersToEnable.indexOf(product) >= 0) { - verify(item.enableAsync(true, resource)).atLeast(1); - } - }); - }); - }); -}); diff --git a/extensions/positron-python/src/test/linters/mypy.unit.test.ts b/extensions/positron-python/src/test/linters/mypy.unit.test.ts deleted file mode 100644 index b697a719a47..00000000000 --- a/extensions/positron-python/src/test/linters/mypy.unit.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { parseLine } from '../../client/linters/baseLinter'; -import { getRegex } from '../../client/linters/mypy'; -import { ILintMessage, LinterId } from '../../client/linters/types'; - -// This following is a real-world example. See gh=2380. - -const output = ` -provider.pyi:10: error: Incompatible types in assignment (expression has type "str", variable has type "int") -provider.pyi:11: error: Name 'not_declared_var' is not defined -provider.pyi:12:21: error: Expression has type "Any" -`; - -suite('Linting - MyPy', () => { - test('regex', async () => { - const lines = output.split('\n'); - const tests: [string, ILintMessage][] = [ - [ - lines[1], - { - code: undefined, - message: 'Incompatible types in assignment (expression has type "str", variable has type "int")', - column: 0, - line: 10, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - [ - lines[2], - { - code: undefined, - message: "Name 'not_declared_var' is not defined", - column: 0, - line: 11, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - [ - lines[3], - { - code: undefined, - message: 'Expression has type "Any"', - column: 20, - line: 12, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, getRegex('provider.pyi'), LinterId.MyPy, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('regex excludes unexpected files', () => { - // mypy run against `foo/bar.py` returning errors for foo/__init__.py - const outputWithUnexpectedFile = `\ -foo/__init__.py:4:5: error: Statement is unreachable [unreachable] -foo/bar.py:2:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] -Found 2 errors in 2 files (checked 1 source file) -`; - - const lines = outputWithUnexpectedFile.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [lines[0], undefined], - [ - lines[1], - { - code: undefined, - message: - 'Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]', - column: 13, - line: 2, - type: 'error', - provider: 'mypy', - }, - ], - [lines[2], undefined], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, getRegex('foo/bar.py'), LinterId.MyPy, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('getRegex escapes filename correctly', () => { - expect(getRegex('foo/bar.py')).to.eql( - String.raw`foo/bar\.py:(?\d+)(:(?\d+))?: (?\w+): (?.*)\r?(\n|$)`, - ); - }); -}); diff --git a/extensions/positron-python/src/test/linters/prompts/flake8Prompt.unit.test.ts b/extensions/positron-python/src/test/linters/prompts/flake8Prompt.unit.test.ts deleted file mode 100644 index 7bbe52ae6d9..00000000000 --- a/extensions/positron-python/src/test/linters/prompts/flake8Prompt.unit.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; -import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import * as windowsApis from '../../../client/common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../../client/ioc/types'; -import * as promptCommons from '../../../client/linters/prompts/common'; -import { Flake8ExtensionPrompt, FLAKE8_EXTENSION } from '../../../client/linters/prompts/flake8Prompt'; -import { IToolsExtensionPrompt } from '../../../client/linters/prompts/types'; - -suite('Flake8 Extension prompt tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let inToolsExtensionsExperimentStub: sinon.SinonStub; - let showInformationMessageStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let serviceContainer: TypeMoq.IMock; - let doNotState: TypeMoq.IMock>; - let appEnv: TypeMoq.IMock; - let prompt: IToolsExtensionPrompt; - - setup(() => { - isExtensionEnabledStub = sinon.stub(promptCommons, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptCommons, 'isExtensionDisabled'); - doNotShowPromptStateStub = sinon.stub(promptCommons, 'doNotShowPromptState'); - inToolsExtensionsExperimentStub = sinon.stub(promptCommons, 'inToolsExtensionsExperiment'); - showInformationMessageStub = sinon.stub(windowsApis, 'showInformationMessage'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - appEnv = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(IApplicationEnvironment)) - .returns(() => appEnv.object); - - doNotState = TypeMoq.Mock.ofType>(); - prompt = new Flake8ExtensionPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Extension already installed and enabled', async () => { - isExtensionEnabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Extension already installed, but disabled', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Test do not show again persistent state', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => true); - doNotShowPromptStateStub.returns(doNotState.object); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User not in experiment', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(false); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User selected: install extension (insiders)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'insiders'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installFlake8Extension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: true, - }); - }); - - test('User selected: install extension (stable)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'stable'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installFlake8Extension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: false, - }); - }); - - test('User selected: do not show again', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - doNotState - .setup((d) => d.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - showInformationMessageStub.resolves(Common.doNotShowAgain); - assert.isFalse(await prompt.showPrompt()); - - doNotState.verifyAll(); - }); - - test('User selected: close', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - showInformationMessageStub.resolves(undefined); - assert.isFalse(await prompt.showPrompt()); - }); -}); diff --git a/extensions/positron-python/src/test/linters/prompts/pylintPrompt.unit.test.ts b/extensions/positron-python/src/test/linters/prompts/pylintPrompt.unit.test.ts deleted file mode 100644 index 65b579f258a..00000000000 --- a/extensions/positron-python/src/test/linters/prompts/pylintPrompt.unit.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; -import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import * as windowsApis from '../../../client/common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../../client/ioc/types'; -import * as promptCommons from '../../../client/linters/prompts/common'; -import { PylintExtensionPrompt, PYLINT_EXTENSION } from '../../../client/linters/prompts/pylintPrompt'; -import { IToolsExtensionPrompt } from '../../../client/linters/prompts/types'; - -suite('Pylint Extension prompt tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let inToolsExtensionsExperimentStub: sinon.SinonStub; - let showInformationMessageStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let serviceContainer: TypeMoq.IMock; - let doNotState: TypeMoq.IMock>; - let appEnv: TypeMoq.IMock; - let prompt: IToolsExtensionPrompt; - - setup(() => { - isExtensionEnabledStub = sinon.stub(promptCommons, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptCommons, 'isExtensionDisabled'); - doNotShowPromptStateStub = sinon.stub(promptCommons, 'doNotShowPromptState'); - inToolsExtensionsExperimentStub = sinon.stub(promptCommons, 'inToolsExtensionsExperiment'); - showInformationMessageStub = sinon.stub(windowsApis, 'showInformationMessage'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - appEnv = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(IApplicationEnvironment)) - .returns(() => appEnv.object); - - doNotState = TypeMoq.Mock.ofType>(); - prompt = new PylintExtensionPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Extension already installed and enabled', async () => { - isExtensionEnabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Extension already installed, but disabled', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('User not in experiment', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(false); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User selected: install extension (insiders)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'insiders'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installPylintExtension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: true, - }); - }); - - test('User selected: install extension (stable)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'stable'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installPylintExtension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: false, - }); - }); - - test('User selected: do not show again', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - doNotState - .setup((d) => d.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - showInformationMessageStub.resolves(Common.doNotShowAgain); - assert.isFalse(await prompt.showPrompt()); - - doNotState.verifyAll(); - }); - - test('User selected: close', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - showInformationMessageStub.resolves(undefined); - assert.isFalse(await prompt.showPrompt()); - }); -}); diff --git a/extensions/positron-python/src/test/linters/pylint.test.ts b/extensions/positron-python/src/test/linters/pylint.test.ts deleted file mode 100644 index e1cec249c66..00000000000 --- a/extensions/positron-python/src/test/linters/pylint.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { Container } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, - DiagnosticSeverity, - TextDocument, - Uri, - WorkspaceConfiguration, - WorkspaceFolder, -} from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { IConfigurationService, IExtensions, IInstaller, IPythonSettings } from '../../client/common/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { LinterManager } from '../../client/linters/linterManager'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterManager } from '../../client/linters/types'; -import { MockLintingSettings } from '../mockClasses'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Linting - Pylint', () => { - let fileSystem: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let execService: TypeMoq.IMock; - let config: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let serviceContainer: ServiceContainer; - let extensionsService: TypeMoq.IMock; - - setup(() => { - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - - platformService = TypeMoq.Mock.ofType(); - platformService.setup((x) => x.isWindows).returns(() => false); - - extensionsService = TypeMoq.Mock.ofType(); - extensionsService.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => undefined); - - workspace = TypeMoq.Mock.ofType(); - execService = TypeMoq.Mock.ofType(); - - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance( - IPythonToolExecutionService, - execService.object, - ); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - serviceManager.addSingletonInstance(IExtensions, extensionsService.object); - - pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - - config = TypeMoq.Mock.ofType(); - config.setup((c) => c.getSettings()).returns(() => pythonSettings.object); - - workspaceConfig = TypeMoq.Mock.ofType(); - workspace.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); - - serviceManager.addSingletonInstance(IConfigurationService, config.object); - const linterManager = new LinterManager(config.object); - serviceManager.addSingletonInstance(ILinterManager, linterManager); - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - }); - - test('Negative column numbers should be treated 0', async () => { - const fileFolder = '/user/a/b/c'; - const pylinter = new Pylint(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - - const document = TypeMoq.Mock.ofType(); - document.setup((x) => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType(); - wsf.setup((x) => x.uri).returns(() => Uri.file(fileFolder)); - - workspace.setup((x) => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); - - const linterOutput = [ - '[', - ' {', - ' "type": "convention",', - ' "module": "test",', - ' "obj": "",', - ' "line": 1,', - ' "column": 1,', - ` "path": "${fileFolder}/test.py",`, - ' "symbol": "missing-module-docstring",', - ' "message": "Missing module docstring",', - ' "message-id": "C0114",', - ' "endLine": null,', - ' "endColumn": null', - ' },', - ' {', - ' "type": "error",', - ' "module": "test",', - ' "obj": "",', - ' "line": 3,', - ' "column": -1,', - ` "path": "${fileFolder}/test.py",`, - ' "symbol": "too-many-format-args",', - ' "message": "Too many arguments for format string",', - ' "message-id": "E1305"', - ' }', - ']', - ].join(os.EOL); - execService - .setup((x) => x.execForLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: linterOutput, stderr: '' })); - - const lintSettings = new MockLintingSettings(); - lintSettings.maxNumberOfProblems = 1000; - lintSettings.pylintPath = 'pyLint'; - lintSettings.pylintEnabled = true; - lintSettings.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }; - - const settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings); - settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - const messages = await pylinter.lint(document.object, new CancellationTokenSource().token); - expect(messages).to.be.lengthOf(2); - expect(messages[0].column).to.be.equal(1); - expect(messages[1].column).to.be.equal(0); - }); -}); diff --git a/extensions/positron-python/src/test/linters/pylint.unit.test.ts b/extensions/positron-python/src/test/linters/pylint.unit.test.ts deleted file mode 100644 index ee6954e870a..00000000000 --- a/extensions/positron-python/src/test/linters/pylint.unit.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService, IExtensions, IPythonSettings } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { IToolsExtensionPrompt } from '../../client/linters/prompts/types'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; - -suite('Pylint - Function runLinter()', () => { - let fileSystem: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let manager: TypeMoq.IMock; - let _info: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let extensionsService: TypeMoq.IMock; - let run: sinon.SinonStub; - let parseMessagesSeverity: sinon.SinonStub; - let extensionPrompt: TypeMoq.IMock; - const doc = { - uri: vscode.Uri.file('path/to/doc'), - }; - const args = [doc.uri.fsPath]; - class PylintTest extends Pylint { - // eslint-disable-next-line class-methods-use-this - public async run( - _args: string[], - _document: vscode.TextDocument, - _cancellation: vscode.CancellationToken, - _regEx: string, - ): Promise { - return []; - } - - // eslint-disable-next-line class-methods-use-this - public parseMessagesSeverity(_error: string, _categorySeverity: unknown): LintMessageSeverity { - return ('Severity' as unknown) as LintMessageSeverity; - } - - // eslint-disable-next-line class-methods-use-this - public get info(): ILinterInfo { - return _info.object; - } - - public async runLinter( - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - ): Promise { - return super.runLinter(document, cancellation); - } - - // eslint-disable-next-line class-methods-use-this - public getWorkingDirectoryPath(_document: vscode.TextDocument): string { - return 'path/to/workspaceRoot'; - } - - public async parseMessages( - output: string, - _document: vscode.TextDocument, - _token: vscode.CancellationToken, - ): Promise { - return super.parseMessages(output, _document, _token, ''); - } - } - - setup(() => { - platformService = TypeMoq.Mock.ofType(); - _info = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - extensionsService = TypeMoq.Mock.ofType(); - manager = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILinterManager))).returns(() => manager.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsService.object); - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - manager.setup((m) => m.getLinterInfo(TypeMoq.It.isAny())).returns(() => (undefined as unknown) as ILinterInfo); - _info.setup((x) => x.id).returns(() => LinterId.PyLint); - extensionPrompt = TypeMoq.Mock.ofType(); - extensionPrompt.setup((e) => e.showPrompt()).returns(() => Promise.resolve(false)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Test pylint with default settings.', async () => { - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); - run = sinon.stub(PylintTest.prototype, 'run'); - run.callsFake(() => Promise.resolve([])); - parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); - parseMessagesSeverity.callsFake(() => 'Severity'); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - await pylint.runLinter(doc as vscode.TextDocument, mock(vscode.CancellationTokenSource).token); - assert.deepEqual(run.args[0][0], args); - assert.ok(parseMessagesSeverity.notCalled); - assert.ok(run.calledOnce); - }); - - test('Message returned by runLinter() is as expected', async () => { - const message = [ - { - type: 'messageType', - }, - ]; - const expectedResult = [ - { - type: 'messageType', - severity: 'LintMessageSeverity', - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); - run = sinon.stub(PylintTest.prototype, 'run'); - run.callsFake(() => Promise.resolve(message)); - parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); - parseMessagesSeverity.callsFake(() => 'LintMessageSeverity'); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.runLinter(doc as vscode.TextDocument, mock(vscode.CancellationTokenSource).token); - assert.deepEqual(result, (expectedResult as unknown) as ILintMessage[]); - assert.ok(parseMessagesSeverity.calledOnce); - assert.ok(run.calledOnce); - }); - - test('Parse json output', async () => { - // If 'endLine' and 'endColumn' are missing in JSON output, - // both should be set to 'undefined' - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: undefined, - endColumn: undefined, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); - - test('Parse json output with endLine', async () => { - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "endLine": 26, - "endColumn": 24, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: 26, - endColumn: 24, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); - - test('Parse json output with unknown endLine', async () => { - // If 'endLine' and 'endColumn' are present in JSON output - // but 'null', 'endLine' should be set to 'undefined'. - // 'endColumn' defaults to 0. - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "endLine": null, - "endColumn": null, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: undefined, - endColumn: undefined, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); -}); diff --git a/extensions/positron-python/src/test/linters/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/linters/serviceRegistry.unit.test.ts deleted file mode 100644 index a27c244af34..00000000000 --- a/extensions/positron-python/src/test/linters/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { IExtensionActivationService } from '../../client/activation/types'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { IServiceManager } from '../../client/ioc/types'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { registerTypes } from '../../client/linters/serviceRegistry'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; - -suite('Linters Service Registry', () => { - let serviceManager: IServiceManager; - - setup(() => { - serviceManager = mock(ServiceManager); - }); - - test('Ensure services are registered', async () => { - registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton(ILintingEngine, LintingEngine)).once(); - verify(serviceManager.addSingleton(ILinterManager, LinterManager)).once(); - verify( - serviceManager.addSingleton(IExtensionActivationService, LinterProvider), - ).once(); - }); -}); diff --git a/extensions/positron-python/src/test/mockClasses.ts b/extensions/positron-python/src/test/mockClasses.ts index c962c4d67ca..e2de7e649b8 100644 --- a/extensions/positron-python/src/test/mockClasses.ts +++ b/extensions/positron-python/src/test/mockClasses.ts @@ -1,12 +1,5 @@ import * as vscode from 'vscode'; import * as util from 'util'; -import { - Flake8CategorySeverity, - ILintingSettings, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, -} from '../client/common/types'; export class MockOutputChannel implements vscode.LogOutputChannel { public name: string; @@ -79,39 +72,3 @@ export class MockStatusBarItem implements vscode.StatusBarItem { public dispose(): void {} } - -export class MockLintingSettings implements ILintingSettings { - public enabled!: boolean; - public cwd?: string; - public ignorePatterns!: string[]; - public prospectorEnabled!: boolean; - public prospectorArgs!: string[]; - public pylintEnabled!: boolean; - public pylintArgs!: string[]; - public pycodestyleEnabled!: boolean; - public pycodestyleArgs!: string[]; - public pylamaEnabled!: boolean; - public pylamaArgs!: string[]; - public flake8Enabled!: boolean; - public flake8Args!: string[]; - public pydocstyleEnabled!: boolean; - public pydocstyleArgs!: string[]; - public lintOnSave!: boolean; - public maxNumberOfProblems!: number; - public pylintCategorySeverity!: IPylintCategorySeverity; - public pycodestyleCategorySeverity!: IPycodestyleCategorySeverity; - public flake8CategorySeverity!: Flake8CategorySeverity; - public mypyCategorySeverity!: IMypyCategorySeverity; - public prospectorPath!: string; - public pylintPath!: string; - public pycodestylePath!: string; - public pylamaPath!: string; - public flake8Path!: string; - public pydocstylePath!: string; - public mypyEnabled!: boolean; - public mypyArgs!: string[]; - public mypyPath!: string; - public banditEnabled!: boolean; - public banditArgs!: string[]; - public banditPath!: string; -} diff --git a/extensions/positron-python/src/test/providers/codeActionProvider/main.unit.test.ts b/extensions/positron-python/src/test/providers/codeActionProvider/main.unit.test.ts index 501c3c7eca2..55644d80ae5 100644 --- a/extensions/positron-python/src/test/providers/codeActionProvider/main.unit.test.ts +++ b/extensions/positron-python/src/test/providers/codeActionProvider/main.unit.test.ts @@ -8,7 +8,6 @@ import rewiremock from 'rewiremock'; import * as typemoq from 'typemoq'; import { CodeActionKind, CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; import { IDisposableRegistry } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; @@ -38,10 +37,7 @@ suite('Code Action Provider service', async () => { }; rewiremock.enable(); rewiremock('vscode').with(vscodeMock); - const quickFixService = new CodeActionProviderService( - typemoq.Mock.ofType().object, - typemoq.Mock.ofType().object, - ); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); await quickFixService.activate(); diff --git a/extensions/positron-python/src/test/providers/prompt/installFormatterPrompt.unit.test.ts b/extensions/positron-python/src/test/providers/prompt/installFormatterPrompt.unit.test.ts deleted file mode 100644 index fbd3a72d8ce..00000000000 --- a/extensions/positron-python/src/test/providers/prompt/installFormatterPrompt.unit.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IPersistentState } from '../../../client/common/types'; -import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import * as extensionsApi from '../../../client/common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { InstallFormatterPrompt } from '../../../client/providers/prompts/installFormatterPrompt'; -import * as promptUtils from '../../../client/providers/prompts/promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from '../../../client/providers/prompts/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; - -suite('Formatter Extension prompt tests', () => { - let inFormatterExtensionExperimentStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let prompt: IInstallFormatterPrompt; - let serviceContainer: TypeMoq.IMock; - let persistState: TypeMoq.IMock>; - let getConfigurationStub: sinon.SinonStub; - let isExtensionEnabledStub: sinon.SinonStub; - let pythonConfig: TypeMoq.IMock; - let editorConfig: TypeMoq.IMock; - let showInformationMessageStub: sinon.SinonStub; - let installFormatterExtensionStub: sinon.SinonStub; - let updateDefaultFormatterStub: sinon.SinonStub; - - setup(() => { - inFormatterExtensionExperimentStub = sinon.stub(promptUtils, 'inFormatterExtensionExperiment'); - inFormatterExtensionExperimentStub.returns(true); - - doNotShowPromptStateStub = sinon.stub(promptUtils, 'doNotShowPromptState'); - persistState = TypeMoq.Mock.ofType>(); - doNotShowPromptStateStub.returns(persistState.object); - - getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); - pythonConfig = TypeMoq.Mock.ofType(); - editorConfig = TypeMoq.Mock.ofType(); - getConfigurationStub.callsFake((section: string) => { - if (section === 'python') { - return pythonConfig.object; - } - return editorConfig.object; - }); - isExtensionEnabledStub = sinon.stub(extensionsApi, 'isExtensionEnabled'); - showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); - installFormatterExtensionStub = sinon.stub(promptUtils, 'installFormatterExtension'); - updateDefaultFormatterStub = sinon.stub(promptUtils, 'updateDefaultFormatter'); - - serviceContainer = TypeMoq.Mock.ofType(); - - prompt = new InstallFormatterPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Not in experiment', async () => { - inFormatterExtensionExperimentStub.returns(false); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(doNotShowPromptStateStub.notCalled); - }); - - test('Do not show was set', async () => { - persistState.setup((p) => p.value).returns(() => true); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(getConfigurationStub.notCalled); - }); - - test('Formatting provider is set to none', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'none'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to yapf', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'yapf'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to autopep8, and autopep8 extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => AUTOPEP8_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to black, and black extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => BLACK_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Prompt: user selects do not show', async () => { - persistState.setup((p) => p.value).returns(() => false); - persistState - .setup((p) => p.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.atLeastOnce()); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves(Common.doNotShowAgain); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - persistState.verifyAll(); - }); - - test('Prompt (autopep8): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (autopep8): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === BLACK_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === AUTOPEP8_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); -}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts index b1925e28442..43effcfa453 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts @@ -213,7 +213,7 @@ suite('CustomVirtualEnvironment Locator', () => { test('onChanged fires if venvPath setting changes', async () => { const events: PythonEnvsChangedEvent[] = []; - const expected: PythonEnvsChangedEvent[] = [{}]; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; locator.onChanged((e) => events.push(e)); await getEnvs(locator.iterEnvs()); @@ -228,7 +228,7 @@ suite('CustomVirtualEnvironment Locator', () => { test('onChanged fires if venvFolders setting changes', async () => { const events: PythonEnvsChangedEvent[] = []; - const expected: PythonEnvsChangedEvent[] = [{}]; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; locator.onChanged((e) => events.push(e)); await getEnvs(locator.iterEnvs()); diff --git a/extensions/positron-python/src/test/pythonFiles/terminalExec/sample_smart_selection.py b/extensions/positron-python/src/test/pythonFiles/terminalExec/sample_smart_selection.py new file mode 100644 index 00000000000..3933f06b5d6 --- /dev/null +++ b/extensions/positron-python/src/test/pythonFiles/terminalExec/sample_smart_selection.py @@ -0,0 +1,21 @@ +my_dict = { + "key1": "value1", + "key2": "value2" +} +#Sample + +print("Audi");print("BMW");print("Mercedes") + +# print("dont print me") + +def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + +# Skip me to prove that you did a good job +def next_func(): + print("You") + diff --git a/extensions/positron-python/src/test/serviceRegistry.ts b/extensions/positron-python/src/test/serviceRegistry.ts index c20a84b1e25..a175b330322 100644 --- a/extensions/positron-python/src/test/serviceRegistry.ts +++ b/extensions/positron-python/src/test/serviceRegistry.ts @@ -33,7 +33,6 @@ import { ITestOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; -import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; import { EnvironmentActivationService } from '../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; import { @@ -46,7 +45,6 @@ import { registerInterpreterTypes } from '../client/interpreter/serviceRegistry' import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; import { LegacyFileSystem } from './legacyFileSystem'; import { MockOutputChannel } from './mockClasses'; @@ -143,14 +141,6 @@ export class IocContainer { unittestsRegisterTypes(this.serviceManager); } - public registerLinterTypes(): void { - lintersRegisterTypes(this.serviceManager); - } - - public registerFormatterTypes(): void { - formattersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } diff --git a/extensions/positron-python/src/test/smoke/smartSend.smoke.test.ts b/extensions/positron-python/src/test/smoke/smartSend.smoke.test.ts new file mode 100644 index 00000000000..20ec70af9b5 --- /dev/null +++ b/extensions/positron-python/src/test/smoke/smartSend.smoke.test.ts @@ -0,0 +1,83 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { assert } from 'chai'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { openFile, waitForCondition } from '../common'; + +suite('Smoke Test: Run Smart Selection and Advance Cursor', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Smart Send', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'create_delete_file.py', + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'smart_send_smoke.txt', + ); + + await fs.remove(outputFile); + + const textDocument = await openFile(file); + + if (vscode.window.activeTextEditor) { + const myPos = new vscode.Position(0, 0); + vscode.window.activeTextEditor!.selections = [new vscode.Selection(myPos, myPos)]; + } + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, 10_000, `"${outputFile}" file not created`); + + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + async function wait() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + }); + } + + await wait(); + + const deletedFile = !(await fs.pathExists(outputFile)); + if (deletedFile) { + assert.ok(true, `"${outputFile}" file has been deleted`); + } else { + assert.fail(`"${outputFile}" file still exists`); + } + }); +}); diff --git a/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts b/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts index 9a46d92c142..d4339a4af61 100644 --- a/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts +++ b/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts @@ -1,49 +1,60 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as sinon from 'sinon'; import { assert } from 'chai'; import { CancellationTokenSource } from 'vscode'; +import { instance, mock } from 'ts-mockito'; import { TensorBoardNbextensionCodeLensProvider } from '../../client/tensorBoard/nbextensionCodeLensProvider'; import { MockDocument } from '../mocks/mockDocument'; +import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; -suite('TensorBoard nbextension code lens provider', () => { - let codeLensProvider: TensorBoardNbextensionCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; +[true, false].forEach((tbExtensionInstalled) => { + suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { + suite('TensorBoard nbextension code lens provider', () => { + let experiment: TensorboardExperiment; + let codeLensProvider: TensorBoardNbextensionCodeLensProvider; + let cancelTokenSource: CancellationTokenSource; - setup(() => { - codeLensProvider = new TensorBoardNbextensionCodeLensProvider([]); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - cancelTokenSource.dispose(); - }); + setup(() => { + sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); + experiment = mock(); + codeLensProvider = new TensorBoardNbextensionCodeLensProvider([], instance(experiment)); + cancelTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancelTokenSource.dispose(); + }); - test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); + test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { + const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); + }); + test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { + const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); + }); + test('Fails when cancellation is signaled', () => { + const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); + cancelTokenSource.cancel(); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); + }); + // Can't verify these cases without running in vscode as we depend on vscode to not call us + // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. + // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { + // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); + // const codeLens = codeLensProvider.provideCodeLenses(document); + // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); + // }); + // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { + // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); + // const codeLens = codeLensProvider.provideCodeLenses(document); + // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); + // }); + }); }); - // Can't verify these cases without running in vscode as we depend on vscode to not call us - // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. - // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); }); diff --git a/extensions/positron-python/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts b/extensions/positron-python/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts index 9b691c9af17..8b16301753a 100644 --- a/extensions/positron-python/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts +++ b/extensions/positron-python/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts @@ -1,58 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as sinon from 'sinon'; import { assert } from 'chai'; import { CancellationTokenSource } from 'vscode'; +import { instance, mock } from 'ts-mockito'; import { TensorBoardImportCodeLensProvider } from '../../client/tensorBoard/tensorBoardImportCodeLensProvider'; import { MockDocument } from '../mocks/mockDocument'; +import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; -suite('TensorBoard import code lens provider', () => { - let codeLensProvider: TensorBoardImportCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; +[true, false].forEach((tbExtensionInstalled) => { + suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { + suite('TensorBoard import code lens provider', () => { + let experiment: TensorboardExperiment; + let codeLensProvider: TensorBoardImportCodeLensProvider; + let cancelTokenSource: CancellationTokenSource; - setup(() => { - codeLensProvider = new TensorBoardImportCodeLensProvider([]); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - cancelTokenSource.dispose(); - }); - [ - 'import tensorboard', - 'import foo, tensorboard', - 'import foo, tensorboard, bar', - 'import tensorboardX', - 'import tensorboardX, bar', - 'import torch.profiler', - 'import foo, torch.profiler', - 'from torch.utils import tensorboard', - 'from torch.utils import foo, tensorboard', - 'import torch.utils.tensorboard, foo', - 'from torch import profiler', - ].forEach((importStatement) => { - test(`Provides code lens for Python files containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, `Failed to provide code lens for file containing ${importStatement} import`); - }); - test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for ipynb containing ${importStatement} import`, - ); + setup(() => { + sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); + experiment = mock(); + codeLensProvider = new TensorBoardImportCodeLensProvider([], instance(experiment)); + cancelTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancelTokenSource.dispose(); + }); + [ + 'import tensorboard', + 'import foo, tensorboard', + 'import foo, tensorboard, bar', + 'import tensorboardX', + 'import tensorboardX, bar', + 'import torch.profiler', + 'import foo, torch.profiler', + 'from torch.utils import tensorboard', + 'from torch.utils import foo, tensorboard', + 'import torch.utils.tensorboard, foo', + 'from torch import profiler', + ].forEach((importStatement) => { + test(`Provides code lens for Python files containing ${importStatement}`, () => { + const document = new MockDocument(importStatement, 'foo.py', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok( + codeLens.length > 0, + `Failed to provide code lens for file containing ${importStatement} import`, + ); + }); + test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { + const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok( + codeLens.length > 0, + `Failed to provide code lens for ipynb containing ${importStatement} import`, + ); + }); + test('Fails when cancellation is signaled', () => { + const document = new MockDocument(importStatement, 'foo.py', async () => true); + cancelTokenSource.cancel(); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); + }); + }); + test('Does not provide code lens if no matching import', () => { + const document = new MockDocument('import foo', 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); + }); }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - }); - test('Does not provide code lens if no matching import', () => { - const document = new MockDocument('import foo', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); }); }); diff --git a/extensions/positron-python/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/extensions/positron-python/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts index b6efad083a5..7eba1805c8b 100644 --- a/extensions/positron-python/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ b/extensions/positron-python/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts @@ -1,91 +1,117 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; +import { anything, instance, mock, reset, when } from 'ts-mockito'; import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; import { MockDocumentManager } from '../mocks/mockDocumentManager'; import { createTensorBoardPromptWithMocks } from './helpers'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; +import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; -suite('TensorBoard usage tracker', () => { - let documentManager: MockDocumentManager; - let tensorBoardImportTracker: TensorBoardUsageTracker; - let prompt: TensorBoardPrompt; - let showNativeTensorBoardPrompt: sinon.SinonSpy; +[true, false].forEach((tbExtensionInstalled) => { + suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { + suite('TensorBoard usage tracker', () => { + let experiment: TensorboardExperiment; + let documentManager: MockDocumentManager; + let tensorBoardImportTracker: TensorBoardUsageTracker; + let prompt: TensorBoardPrompt; + let showNativeTensorBoardPrompt: sinon.SinonSpy; - setup(() => { - documentManager = new MockDocumentManager(); - prompt = createTensorBoardPromptWithMocks(); - showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); - tensorBoardImportTracker = new TensorBoardUsageTracker(documentManager, [], prompt); - }); + suiteSetup(() => { + reset(mockedVSCodeNamespaces.extensions); + when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); + }); + suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); + setup(() => { + sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); + experiment = mock(); + documentManager = new MockDocumentManager(); + prompt = createTensorBoardPromptWithMocks(); + showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); + tensorBoardImportTracker = new TensorBoardUsageTracker( + documentManager, + [], + prompt, + instance(experiment), + ); + }); - test('Simple tensorboard import in Python file', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboardX import in Python file', async () => { - const document = documentManager.addDocument('import tensorboardX', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboard import in Python ipynb', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y.tensorboard import z` import', async () => { - const document = documentManager.addDocument('from torch.utils.tensorboard import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y import tensorboard` import', async () => { - const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from tensorboardX import x` import', async () => { - const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import x, y` import', async () => { - const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import pkg as _` import', async () => { - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Show prompt on changed text editor', async () => { - await tensorBoardImportTracker.activate(); - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Do not show prompt if no tensorboard import', async () => { - const document = documentManager.addDocument('import tensorflow as tf\nfrom torch.utils import foo', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - test('Do not show prompt if language is not Python', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.cpp', - 'cpp', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); + test('Simple tensorboard import in Python file', async () => { + const document = documentManager.addDocument('import tensorboard', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Simple tensorboardX import in Python file', async () => { + const document = documentManager.addDocument('import tensorboardX', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Simple tensorboard import in Python ipynb', async () => { + const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`from x.y.tensorboard import z` import', async () => { + const document = documentManager.addDocument( + 'from torch.utils.tensorboard import SummaryWriter', + 'foo.py', + ); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`from x.y import tensorboard` import', async () => { + const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`from tensorboardX import x` import', async () => { + const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`import x, y` import', async () => { + const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`import pkg as _` import', async () => { + const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Show prompt on changed text editor', async () => { + await tensorBoardImportTracker.activate(); + const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); + await documentManager.showTextDocument(document); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Do not show prompt if no tensorboard import', async () => { + const document = documentManager.addDocument( + 'import tensorflow as tf\nfrom torch.utils import foo', + 'foo.py', + ); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.notCalled); + }); + test('Do not show prompt if language is not Python', async () => { + const document = documentManager.addDocument( + 'import tensorflow as tf\nfrom torch.utils import foo', + 'foo.cpp', + 'cpp', + ); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.notCalled); + }); + }); }); }); diff --git a/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts b/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts new file mode 100644 index 00000000000..8d70ab6e01e --- /dev/null +++ b/extensions/positron-python/src/test/terminals/codeExecution/smartSend.test.ts @@ -0,0 +1,229 @@ +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { TextEditor, Selection, Position, TextDocument } from 'vscode'; +import * as fs from 'fs-extra'; +import { SemVer } from 'semver'; +import { assert, expect } from 'chai'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IConfigurationService, IExperimentService } from '../../../client/common/types'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { EnableREPLSmartSend } from '../../../client/common/experiments/groups'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PYTHON_PATH } from '../../common'; +import { Architecture } from '../../../client/common/utils/platform'; +import { ProcessService } from '../../../client/common/process/proc'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); + +suite('REPL - Smart Send', () => { + let documentManager: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + + let processServiceFactory: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + + let serviceContainer: TypeMoq.IMock; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock; + + let processService: TypeMoq.IMock; + + let document: TypeMoq.IMock; + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + + // suite set up only run once for each suite. Very start + // set up --- before each test + // tests -- actual tests + // tear down -- run after each test + // suite tear down only run once at the very end. + + // all object that is common to every test. What each test needs + setup(() => { + documentManager = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((x: any) => x.then).returns(() => undefined); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService))) + .returns(() => experimentService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + + codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); + document = TypeMoq.Mock.ofType(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.never()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + commandManager.verifyAll(); + }); + + test('Smart send should perform smart selection and move cursor', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + + // Do not perform smart selection when there is explicit selection + test('Smart send should not perform smart selection when there is explicit selection', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualNonSmartResult = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic + expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); + }); +}); diff --git a/extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts new file mode 100644 index 00000000000..f775241abb3 --- /dev/null +++ b/extensions/positron-python/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, TerminalDataWriteEvent, TextDocument, TextEditor, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { + IApplicationEnvironment, + IApplicationShell, + IDocumentManager, + ITerminalManager, +} from '../../../client/common/application/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import * as processApi from '../../../client/common/process/rawProcessApis'; +import * as fsapi from '../../../client/common/platform/fs-paths'; +import { noop } from '../../../client/common/utils/misc'; + +suite('Terminal Deactivation Limitation Prompt', () => { + let shell: IApplicationShell; + let experimentService: IExperimentService; + let persistentStateFactory: IPersistentStateFactory; + let appEnvironment: IApplicationEnvironment; + let deactivatePrompt: TerminalDeactivateLimitationPrompt; + let terminalWriteEvent: EventEmitter; + let notificationEnabled: IPersistentState; + let interpreterService: IInterpreterService; + let terminalManager: ITerminalManager; + let documentManager: IDocumentManager; + const prompts = [Common.editSomething.format('~/.bashrc'), Common.doNotShowAgain]; + const expectedMessage = Interpreters.terminalDeactivatePrompt.format('~/.bashrc'); + const initScriptPath = 'home/node/.bashrc'; + const resource = Uri.file('a'); + let terminal: Terminal; + + setup(async () => { + const activeEditorEvent = new EventEmitter(); + const document = ({ + uri: Uri.file(''), + getText: () => '', + } as unknown) as TextDocument; + sinon.stub(processApi, 'shellExec').callsFake(async (command: string) => { + if (command !== 'echo ~/.bashrc') { + throw new Error(`Unexpected command: ${command}`); + } + await sleep(1500); + return { stdout: initScriptPath }; + }); + documentManager = mock(); + terminalManager = mock(); + terminal = ({ + creationOptions: { cwd: resource }, + processId: Promise.resolve(1), + dispose: noop, + show: noop, + sendText: noop, + } as unknown) as Terminal; + when(terminalManager.createTerminal(anything())).thenReturn(terminal); + when(documentManager.openTextDocument(initScriptPath)).thenReturn(Promise.resolve(document)); + when(documentManager.onDidChangeActiveTextEditor).thenReturn(activeEditorEvent.event); + shell = mock(); + interpreterService = mock(); + experimentService = mock(); + persistentStateFactory = mock(); + appEnvironment = mock(); + when(appEnvironment.shell).thenReturn('bash'); + notificationEnabled = mock>(); + terminalWriteEvent = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + deactivatePrompt = new TerminalDeactivateLimitationPrompt( + instance(shell), + instance(persistentStateFactory), + [], + instance(interpreterService), + instance(appEnvironment), + instance(documentManager), + instance(terminalManager), + instance(experimentService), + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + }); + + test('When using cmd, do not show notification for the same', async () => { + reset(appEnvironment); + when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + when(notificationEnabled.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when virtual env is not activated for terminal', async () => { + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Edit script correctly if `Edit