From 08628702e95a34bb4f9fa508c799db5f320c415d Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 13 Jul 2021 16:11:53 -0700 Subject: [PATCH 1/9] Fix 'Cannot read property 'resolveEnv' of undefined' error (#16677) --- .../pythonEnvironments/base/locators/common/wrappingLocator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/pythonEnvironments/base/locators/common/wrappingLocator.ts b/src/client/pythonEnvironments/base/locators/common/wrappingLocator.ts index 7431fc217b77..9cd1bffb0a09 100644 --- a/src/client/pythonEnvironments/base/locators/common/wrappingLocator.ts +++ b/src/client/pythonEnvironments/base/locators/common/wrappingLocator.ts @@ -32,6 +32,7 @@ export class LazyWrappingLocator extends LazyResourceBasedLocator { } public async resolveEnv(env: string): Promise { + await this.ensureResourcesReady(); return this.wrapped!.resolveEnv(env); } From 03df7c261538cf84e790ab471e6032485ffaa002 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 16 Jul 2021 10:13:29 -0700 Subject: [PATCH 2/9] Cherry pick fixes into release (#16686) * Bump actions/setup-node from 2.1.5 to 2.2.0 (#16592) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.1.5 to 2.2.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v2.1.5...v2.2.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add support for starting TensorBoard session with a remote log directory via URL (#16477) * Add support for remote logdirs * Add missing keys * Fix typo * Bump isort from 5.8.0 to 5.9.2 (#16636) Bumps [isort](https://github.com/pycqa/isort) from 5.8.0 to 5.9.2. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.8.0...5.9.2) --- updated-dependencies: - dependency-name: isort dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Make getInterpreters() API faster for subsequent calls (#16674) * Change the way auto-selection works (#16644) * New comparison logic * Add experiment group * Register and call it * Add service registry tests * Add interpreter selector unit tests * Add comparison unit tests * Add intepreter selector test * News file * Adjust comments * Reuse getSortName * Add new auto-selection logic * Add tests for getEnvTypeHeuristic * Move persistent store initialization back out * Update tests Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joyce Er Co-authored-by: Kartik Raj Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> --- .github/workflows/build.yml | 12 +- .github/workflows/nightly-coverage.yml | 2 +- .github/workflows/pr-check.yml | 16 +- news/1 Enhancements/16461.md | 1 + package.nls.json | 4 +- requirements.in | 2 +- requirements.txt | 6 +- src/client/common/experiments/helpers.ts | 7 + src/client/common/utils/localize.ts | 5 + src/client/interpreter/autoSelection/index.ts | 69 ++++++- .../configuration/environmentTypeComparer.ts | 4 +- src/client/interpreter/interpreterService.ts | 4 +- src/client/tensorBoard/tensorBoardSession.ts | 28 ++- .../environmentTypeComparer.unit.test.ts | 67 ++++++- .../autoSelection/index.unit.test.ts | 183 ++++++++++++++---- .../tensorBoard/tensorBoardSession.test.ts | 17 +- 16 files changed, 356 insertions(+), 71 deletions(-) create mode 100644 news/1 Enhancements/16461.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ac76f2b3607..ade9d53d4fad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -85,7 +85,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -139,7 +139,7 @@ jobs: path: ${{env.special-working-directory-relative}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -155,7 +155,7 @@ jobs: python-version: ${{matrix.python}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -340,7 +340,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -404,7 +404,7 @@ jobs: # uses: actions/checkout@v2.3.4 # - name: Install Node - # uses: actions/setup-node@v2.1.5 + # uses: actions/setup-node@v2.2.0 # with: # node-version: ${{env.NODE_VERSION}} diff --git a/.github/workflows/nightly-coverage.yml b/.github/workflows/nightly-coverage.yml index 8389bcb45ff8..4c2418989db4 100644 --- a/.github/workflows/nightly-coverage.yml +++ b/.github/workflows/nightly-coverage.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 00dc3840b4be..a9da09a9af15 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -118,7 +118,7 @@ jobs: path: ${{env.special-working-directory-relative}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -134,7 +134,7 @@ jobs: python-version: ${{matrix.python}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -323,7 +323,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -389,7 +389,7 @@ jobs: # uses: actions/checkout@v2.3.4 # - name: Install Node - # uses: actions/setup-node@v2.1.5 + # uses: actions/setup-node@v2.2.0 # with: # node-version: ${{env.NODE_VERSION}} @@ -452,7 +452,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -468,7 +468,7 @@ jobs: python-version: ${{env.PYTHON_VERSION}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} diff --git a/news/1 Enhancements/16461.md b/news/1 Enhancements/16461.md new file mode 100644 index 000000000000..58f5ca182794 --- /dev/null +++ b/news/1 Enhancements/16461.md @@ -0,0 +1 @@ +Support starting a TensorBoard session with a remote URL hosting log files. \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 96ee3df8c1e9..0940a7309557 100644 --- a/package.nls.json +++ b/package.nls.json @@ -251,5 +251,7 @@ "TensorBoard.installProfilerPluginPrompt": "We recommend installing the PyTorch Profiler TensorBoard plugin. Would you like to install the package?", "TensorBoard.upgradePrompt": "Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?", "TensorBoard.launchNativeTensorBoardSessionCodeAction": "Launch TensorBoard session", - "TensorBoard.launchNativeTensorBoardSessionCodeLens": "▶ Launch TensorBoard Session" + "TensorBoard.launchNativeTensorBoardSessionCodeLens": "▶ Launch TensorBoard Session", + "TensorBoard.enterRemoteUrl": "Enter remote URL", + "TensorBoard.enterRemoteUrlDetail": "Enter a URL pointing to a remote directory containing your TensorBoard log files" } diff --git a/requirements.in b/requirements.in index c6b52697690c..b00540bbb513 100644 --- a/requirements.in +++ b/requirements.in @@ -6,4 +6,4 @@ # IntelliSense via Jedi jedi<0.18 # For Python 2.7 support # Sort Imports -isort==5.8.0; python_version >= '3.6' +isort==5.9.2; python_version >= '3.6' diff --git a/requirements.txt b/requirements.txt index c4cdd1a4f6a0..0a83b881cf1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --generate-hashes requirements.in # -isort==5.8.0 ; python_version >= "3.6" \ - --hash=sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6 \ - --hash=sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d +isort==5.9.2 ; python_version >= "3.6" \ + --hash=sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813 \ + --hash=sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e # via -r requirements.in jedi==0.17.2 \ --hash=sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20 \ diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index a85a468f494f..127559f4fb12 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -13,3 +13,10 @@ export async function inDiscoveryExperiment(experimentService: IExperimentServic ]); return results.includes(true); } + +export function inDiscoveryExperimentSync(experimentService: IExperimentService): boolean { + return ( + experimentService.inExperimentSync(DiscoveryVariants.discoverWithFileWatching) || + experimentService.inExperimentSync(DiscoveryVariants.discoveryWithoutFileWatching) + ); +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e7a8e664d32e..82bfacd3fee6 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -139,6 +139,11 @@ export namespace Jupyter { } export namespace TensorBoard { + export const enterRemoteUrl = localize('TensorBoard.enterRemoteUrl', 'Enter remote URL'); + export const enterRemoteUrlDetail = localize( + 'TensorBoard.enterRemoteUrlDetail', + 'Enter a URL pointing to a remote directory containing your TensorBoard log files', + ); export const useCurrentWorkingDirectoryDetail = localize( 'TensorBoard.useCurrentWorkingDirectoryDetail', 'TensorBoard will search for tfevent files in all subdirectories of the current working directory', diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index 5bc13628e3b5..1340c0c4daee 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -6,15 +6,18 @@ import { inject, injectable, named } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import { EnvironmentSorting } from '../../common/experiments/groups'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IInterpreterHelper } from '../contracts'; +import { EnvTypeHeuristic, getEnvTypeHeuristic } from '../configuration/environmentTypeComparer'; +import { InterpreterComparisonType, IInterpreterComparer } from '../configuration/types'; +import { IInterpreterHelper, IInterpreterService } from '../contracts'; import { AutoSelectionRule, IInterpreterAutoSelectionRule, @@ -46,6 +49,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IInterpreterComparer) + @named(InterpreterComparisonType.EnvType) + private readonly envTypeComparer: IInterpreterComparer, @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.systemWide) systemInterpreter: IInterpreterAutoSelectionRule, @@ -104,19 +112,30 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio winRegInterpreter.setNextRule(systemInterpreter); } + /** + * If there's a cached auto-selected interpreter -> return it. + * If not, check if we are in the env sorting experiment, and use the appropriate auto-selection logic. + */ @captureTelemetry(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { rule: AutoSelectionRule.all }, true) public async autoSelectInterpreter(resource: Resource): Promise { const key = this.getWorkspacePathKey(resource); + if (!this.autoSelectedWorkspacePromises.has(key)) { const deferred = createDeferred(); this.autoSelectedWorkspacePromises.set(key, deferred); + await this.initializeStore(resource); await this.clearWorkspaceStoreIfInvalid(resource); - await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); - this.didAutoSelectedInterpreterEmitter.fire(); - Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors(); + + if (await this.experimentService.inExperiment(EnvironmentSorting.experiment)) { + await this.autoselectInterpreterWithLocators(resource); + } else { + await this.autoselectInterpreterWithRules(resource); + } + deferred.resolve(); } + return this.autoSelectedWorkspacePromises.get(key)!.promise; } @@ -221,4 +240,44 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } return undefined; } + + private async autoselectInterpreterWithRules(resource: Resource): Promise { + await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); + + this.didAutoSelectedInterpreterEmitter.fire(); + + Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors(); + } + + /** + * Auto-selection logic: + * 1. If there are cached interpreters (not the first session in this workspace) + * -> sort using the same logic as in the interpreter quickpick and return the first one; + * 2. If not, we already fire all the locators, so wait for their response, sort the interpreters and return the first one. + * + * `getInterpreters` will check the cache first and return early if there are any cached interpreters, + * and if not it will wait for locators to return. + * As such, we can sort interpreters based on what it returns. + */ + private async autoselectInterpreterWithLocators(resource: Resource): Promise { + const interpreters = await this.interpreterService.getInterpreters(resource); + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + + // When auto-selecting an intepreter for a workspace, we either want to return a local one + // or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment + // because we would have to add a way to match environments to a workspace. + const filteredInterpreters = interpreters.filter( + (i) => getEnvTypeHeuristic(i, workspaceUri?.folderUri.fsPath || '') !== EnvTypeHeuristic.Global, + ); + + filteredInterpreters.sort(this.envTypeComparer.compare.bind(this.envTypeComparer)); + + if (workspaceUri) { + this.setWorkspaceInterpreter(workspaceUri.folderUri, filteredInterpreters[0]); + } else { + this.setGlobalInterpreter(filteredInterpreters[0]); + } + + this.didAutoSelectedInterpreterEmitter.fire(); + } } diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index bf73d96d00bd..ac0eaae0cd9a 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -15,7 +15,7 @@ import { IInterpreterComparer } from './types'; * - Global environments (pipenv, conda); * - Globally-installed interpreters (/usr/bin/python3, Windows Store). */ -enum EnvTypeHeuristic { +export enum EnvTypeHeuristic { Local = 1, Global = 2, GlobalInterpreters = 3, @@ -165,7 +165,7 @@ function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment, work /** * Return a heuristic value depending on the environment type. */ -function getEnvTypeHeuristic(environment: PythonEnvironment, workspacePath: string): EnvTypeHeuristic { +export function getEnvTypeHeuristic(environment: PythonEnvironment, workspacePath: string): EnvTypeHeuristic { const { envType } = environment; if (workspacePath.length > 0 && environment.envPath && isParentPath(environment.envPath, workspacePath)) { diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index ea5da0d2aa44..492d0a4dda7e 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -33,7 +33,7 @@ import { } from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; import { getInterpreterHash } from '../pythonEnvironments/discovery/locators/services/hashProvider'; -import { inDiscoveryExperiment } from '../common/experiments/helpers'; +import { inDiscoveryExperiment, inDiscoveryExperimentSync } from '../common/experiments/helpers'; import { StopWatch } from '../common/utils/stopWatch'; import { PythonVersion } from '../pythonEnvironments/info/pythonVersion'; @@ -139,7 +139,7 @@ export class InterpreterService implements Disposable, IInterpreterService { public async getInterpreters(resource?: Uri, options?: GetInterpreterOptions): Promise { let environments: PythonEnvironment[] = []; const stopWatch = new StopWatch(); - if (await inDiscoveryExperiment(this.experimentService)) { + if (inDiscoveryExperimentSync(this.experimentService)) { environments = await this.pyenvs.getInterpreters(resource, options); } else { const locator = this.serviceContainer.get( diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 6f66343be2f2..814b739ebc1c 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -290,7 +290,6 @@ export class TensorBoardSession { return tensorboardInstallStatus === ProductInstallStatus.Installed; } - // eslint-disable-next-line class-methods-use-this private async showFilePicker(): Promise { const selection = await this.applicationShell.showOpenDialog({ canSelectFiles: false, @@ -307,6 +306,8 @@ export class TensorBoardSession { // eslint-disable-next-line class-methods-use-this private getQuickPickItems(logDir: string | undefined) { + const items = []; + if (logDir) { const useCwd = { label: TensorBoard.useCurrentWorkingDirectory(), @@ -316,13 +317,21 @@ export class TensorBoardSession { label: TensorBoard.selectAnotherFolder(), detail: TensorBoard.selectAnotherFolderDetail(), }; - return [useCwd, selectAnotherFolder]; + items.push(useCwd, selectAnotherFolder); + } else { + const selectAFolder = { + label: TensorBoard.selectAFolder(), + detail: TensorBoard.selectAFolderDetail(), + }; + items.push(selectAFolder); } - const selectAFolder = { - label: TensorBoard.selectAFolder(), - detail: TensorBoard.selectAFolderDetail(), - }; - return [selectAFolder]; + + items.push({ + label: TensorBoard.enterRemoteUrl(), + detail: TensorBoard.enterRemoteUrlDetail(), + }); + + return items; } // Display a quickpick asking the user to acknowledge our autopopulated log directory or @@ -341,6 +350,7 @@ export class TensorBoardSession { const useCurrentWorkingDirectory = TensorBoard.useCurrentWorkingDirectory(); const selectAFolder = TensorBoard.selectAFolder(); const selectAnotherFolder = TensorBoard.selectAnotherFolder(); + const enterRemoteUrl = TensorBoard.enterRemoteUrl(); const items: QuickPickItem[] = this.getQuickPickItems(logDir); const item = await this.applicationShell.showQuickPick(items, { canPickMany: false, @@ -353,6 +363,10 @@ export class TensorBoardSession { case selectAFolder: case selectAnotherFolder: return this.showFilePicker(); + case enterRemoteUrl: + return this.applicationShell.showInputBox({ + prompt: TensorBoard.enterRemoteUrlDetail(), + }); default: return undefined; } diff --git a/src/test/configuration/environmentTypeComparer.unit.test.ts b/src/test/configuration/environmentTypeComparer.unit.test.ts index 66699006dd60..ef4fc630e5f6 100644 --- a/src/test/configuration/environmentTypeComparer.unit.test.ts +++ b/src/test/configuration/environmentTypeComparer.unit.test.ts @@ -4,7 +4,11 @@ import * as assert from 'assert'; import * as path from 'path'; import * as sinon from 'sinon'; -import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer'; +import { + EnvironmentTypeComparer, + EnvTypeHeuristic, + getEnvTypeHeuristic, +} from '../../client/interpreter/configuration/environmentTypeComparer'; import { IInterpreterHelper } from '../../client/interpreter/contracts'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; @@ -225,3 +229,64 @@ suite('Environment sorting', () => { }); }); }); + +suite('getEnvTypeHeuristic tests', () => { + const workspacePath = path.join('path', 'to', 'workspace'); + + const localGlobalEnvTypes = [ + EnvironmentType.Venv, + EnvironmentType.Conda, + EnvironmentType.VirtualEnv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Pipenv, + EnvironmentType.Poetry, + ]; + + localGlobalEnvTypes.forEach((envType) => { + test('If the path to an environment starts with the workspace path it should be marked as local', () => { + const environment = { + envType, + envPath: path.join(workspacePath, 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.Local); + }); + + test('If the path to an environment does not start with the workspace path it should be marked as global', () => { + const environment = { + envType, + envPath: path.join('path', 'to', 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.Global); + }); + }); + + const globalInterpretersEnvTypes = [ + EnvironmentType.System, + EnvironmentType.WindowsStore, + EnvironmentType.Global, + EnvironmentType.Unknown, + EnvironmentType.Pyenv, + ]; + + globalInterpretersEnvTypes.forEach((envType) => { + test(`If the environment type is ${envType} and the environment path does not start with the workspace path it should be marked as a global interpreter`, () => { + const environment = { + envType, + envPath: path.join('path', 'to', 'a', 'global', 'interpreter'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.GlobalInterpreters); + }); + }); +}); diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index 4c05d91afd5e..e56db655139b 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -4,11 +4,13 @@ 'use strict'; import { expect } from 'chai'; +import * as path from 'path'; import { SemVer } from 'semver'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; +import { EnvironmentSorting } from '../../../client/common/experiments/groups'; import { ExperimentService } from '../../../client/common/experiments/service'; import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; import { FileSystem } from '../../../client/common/platform/fileSystem'; @@ -27,9 +29,11 @@ import { IInterpreterAutoSelectionRule, IInterpreterAutoSelectionProxyService, } from '../../../client/interpreter/autoSelection/types'; -import { IInterpreterHelper } from '../../../client/interpreter/contracts'; +import { EnvironmentTypeComparer } from '../../../client/interpreter/configuration/environmentTypeComparer'; +import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -50,6 +54,7 @@ suite('Interpreters - Auto Selection', () => { let helper: IInterpreterHelper; let proxy: IInterpreterAutoSelectionProxyService; let experiments: IExperimentService; + let interpreterService: IInterpreterService; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { public initializeStore(resource: Resource): Promise { return super.initializeStore(resource); @@ -77,12 +82,19 @@ suite('Interpreters - Auto Selection', () => { helper = mock(InterpreterHelper); proxy = mock(InterpreterAutoSelectionProxyService); experiments = mock(ExperimentService); + interpreterService = mock(InterpreterService); + + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + when(experiments.inExperimentSync(anything())).thenReturn(false); autoSelectionService = new InterpreterAutoSelectionServiceTest( instance(workspaceService), instance(stateFactory), instance(fs), + instance(experiments), + instance(interpreterService), + interpreterComparer, instance(systemInterpreter), instance(currentPathInterpreter), instance(winRegInterpreter), @@ -105,44 +117,143 @@ suite('Interpreters - Auto Selection', () => { verify(winRegInterpreter.setNextRule(instance(systemInterpreter))).once(); verify(systemInterpreter.setNextRule(anything())).never(); }); - test('Run rules in background', async () => { - let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { - eventFired = true; - }); - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); - expect(eventFired).to.deep.equal(true, 'event not fired'); + suite('When using rule-based auto-selection', () => { + setup(() => { + when(experiments.inExperiment(EnvironmentSorting.experiment)).thenResolve(false); + }); - const allRules = [ - userDefinedInterpreter, - winRegInterpreter, - currentPathInterpreter, - systemInterpreter, - workspaceInterpreter, - cachedPaths, - ]; - for (const service of allRules) { - verify(service.autoSelectInterpreter(undefined)).once(); - if (service !== userDefinedInterpreter) { - verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); + test('Run rules in background', async () => { + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + + const allRules = [ + userDefinedInterpreter, + winRegInterpreter, + currentPathInterpreter, + systemInterpreter, + workspaceInterpreter, + cachedPaths, + ]; + for (const service of allRules) { + verify(service.autoSelectInterpreter(undefined)).once(); + if (service !== userDefinedInterpreter) { + verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); + } } - } - verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + }); + + test('Run userDefineInterpreter as the first rule', async () => { + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); + }); }); - test('Run userDefineInterpreter as the first rule', async () => { - let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { - eventFired = true; + + suite('When using locator-based auto-selection', () => { + let workspacePath: string; + let resource: Uri; + let eventFired: boolean; + + setup(() => { + workspacePath = path.join('path', 'to', 'workspace'); + resource = Uri.parse('resource'); + eventFired = false; + + const folderUri = { fsPath: workspacePath }; + + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ + folderUri, + } as WorkspacePythonPath); + when( + stateFactory.createWorkspacePersistentState(anyString(), undefined), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), '')).thenReturn('workspaceIdentifier'); + when(experiments.inExperiment(EnvironmentSorting.experiment)).thenResolve(true); + + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); }); - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); + test('If there is a local environment select it', async () => { + const localEnv = { + envType: EnvironmentType.Venv, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenResolve([ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment, + localEnv, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(localEnv)).once(); + }); - expect(eventFired).to.deep.equal(true, 'event not fired'); - verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); + test('If there are no local environments, return a globally-installed interpreter', async () => { + const systemEnv = { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenResolve([ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + systemEnv, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(systemEnv)).once(); + }); }); + test('Initialize the store', async () => { let initialize = false; let eventFired = false; @@ -200,6 +311,12 @@ suite('Interpreters - Auto Selection', () => { undefined, ), ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(true); diff --git a/src/test/tensorBoard/tensorBoardSession.test.ts b/src/test/tensorBoard/tensorBoardSession.test.ts index 2ed76ac69419..23c10a91aef9 100644 --- a/src/test/tensorBoard/tensorBoardSession.test.ts +++ b/src/test/tensorBoard/tensorBoardSession.test.ts @@ -141,7 +141,6 @@ suite('TensorBoard session creation', async () => { assert.ok(daemon?.killed, 'TensorBoard session process not killed after webview closed'); }); test('When user selects file picker, display file picker', async () => { - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); // Stub user selections sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAnotherFolder() }); const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog'); @@ -155,6 +154,22 @@ suite('TensorBoard session creation', async () => { assert.ok(filePickerStub.called, 'User requests to select another folder and file picker was not shown'); }); + test('When user selects remote URL, display input box', async () => { + sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.enterRemoteUrl() }); + const inputBoxStub = sandbox.stub(applicationShell, 'showInputBox'); + + // Create session + await commandManager.executeCommand( + 'python.launchTensorBoard', + TensorBoardEntrypoint.palette, + TensorBoardEntrypointTrigger.palette, + ); + + assert.ok( + inputBoxStub.called, + 'User requested to enter remote URL and input box to enter URL was not shown', + ); + }); }); suite('Installation prompt message', async () => { async function createSessionAndVerifyMessage(message: string) { From 2e5d9f78e757a57fdc70c746f43f06ac97ddf849 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 19 Jul 2021 12:53:49 -0700 Subject: [PATCH 3/9] Change version for release (#16722) --- CHANGELOG.md | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a61b31aebe..edc6e3711e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2021.7.0-rc (12 July 2021) +## 2021.7.0 (19 July 2021) ### Enhancements diff --git a/package-lock.json b/package-lock.json index df130973894f..8c81bb9aa786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "python", - "version": "2021.7.0-rc", + "version": "2021.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f3c43326d9a7..853a41ba84fd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2021.7.0-rc", + "version": "2021.7.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 0263703aa5df4b70b3a0ca53cf1c5350465274e5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Jul 2021 10:52:24 -0700 Subject: [PATCH 4/9] Ensure we block on autoselection when no interpreter is explictly set by user (#16723) * Ensure we block on autoselection when no interpreter is explictly set by user * Added tests * News entry --- news/2 Fixes/16723.md | 1 + .../common/interpreterPathProxyService.ts | 33 ++++++++++++ src/client/common/serviceRegistry.ts | 6 +++ src/client/common/types.ts | 8 +++ src/client/interpreter/display/index.ts | 16 ++++-- .../interpreterPathProxyService.unit.test.ts | 54 +++++++++++++++++++ src/test/interpreters/display.unit.test.ts | 16 +++--- 7 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 news/2 Fixes/16723.md create mode 100644 src/client/common/interpreterPathProxyService.ts create mode 100644 src/test/common/interpreterPathProxyService.unit.test.ts diff --git a/news/2 Fixes/16723.md b/news/2 Fixes/16723.md new file mode 100644 index 000000000000..75b29c7e8244 --- /dev/null +++ b/news/2 Fixes/16723.md @@ -0,0 +1 @@ +Ensure we block on autoselection when no interpreter is explictly set by user. diff --git a/src/client/common/interpreterPathProxyService.ts b/src/client/common/interpreterPathProxyService.ts new file mode 100644 index 000000000000..f1044747a1d0 --- /dev/null +++ b/src/client/common/interpreterPathProxyService.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IWorkspaceService } from './application/types'; +import { DeprecatePythonPath } from './experiments/groups'; +import { IExperimentService, IInterpreterPathProxyService, IInterpreterPathService, Resource } from './types'; +import { SystemVariables } from './variables/systemVariables'; + +@injectable() +export class InterpreterPathProxyService implements IInterpreterPathProxyService { + constructor( + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + @inject(IExperimentService) private readonly experiment: IExperimentService, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + ) {} + + public get(resource: Resource): string { + const systemVariables = new SystemVariables( + undefined, + this.workspace.getWorkspaceFolder(resource)?.uri.fsPath, + this.workspace, + ); + const pythonSettings = this.workspace.getConfiguration('python', resource); + return systemVariables.resolveAny( + this.experiment.inExperimentSync(DeprecatePythonPath.experiment) + ? this.interpreterPathService.get(resource) + : pythonSettings.get('pythonPath'), + )!; + } +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 66b37072cff5..16a4d99fd49c 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -20,6 +20,7 @@ import { IToolExecutionPath, IsWindows, ToolExecutionPath, + IInterpreterPathProxyService, } from './types'; import { IServiceManager } from '../ioc/types'; import { JupyterExtensionDependencyManager } from '../jupyter/jupyterExtensionDependencyManager'; @@ -118,11 +119,16 @@ import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStep import { Random } from './utils/random'; import { JupyterNotInstalledNotificationHelper } from '../jupyter/jupyterNotInstalledNotificationHelper'; import { IJupyterNotInstalledNotificationHelper } from '../jupyter/types'; +import { InterpreterPathProxyService } from './interpreterPathProxyService'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); + serviceManager.addSingleton( + IInterpreterPathProxyService, + InterpreterPathProxyService, + ); serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); serviceManager.addSingleton(IExtensions, Extensions); serviceManager.addSingleton(IRandom, Random); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 8b9e423dea73..bf437ff3d493 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -557,6 +557,14 @@ export interface IInterpreterPathService { copyOldInterpreterStorageValuesToNew(resource: Uri | undefined): Promise; } +/** + * Interface used to access current Interpreter Path + */ +export const IInterpreterPathProxyService = Symbol('IInterpreterPathProxyService'); +export interface IInterpreterPathProxyService { + get(resource: Resource): string; +} + export type DefaultLSType = LanguageServerType.Jedi | LanguageServerType.JediLSP | LanguageServerType.Node; /** diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 4e381031fad8..2e1f76e3538b 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -3,7 +3,13 @@ import { Disposable, OutputChannel, StatusBarAlignment, StatusBarItem, Uri } fro import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import '../../common/extensions'; -import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPathUtils, Resource } from '../../common/types'; +import { + IDisposableRegistry, + IInterpreterPathProxyService, + IOutputChannel, + IPathUtils, + Resource, +} from '../../common/types'; import { Interpreters } from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -22,7 +28,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { private readonly workspaceService: IWorkspaceService; private readonly pathUtils: IPathUtils; private readonly interpreterService: IInterpreterService; - private readonly configService: IConfigurationService; + private readonly interpreterPathExpHelper: IInterpreterPathProxyService; private currentlySelectedInterpreterPath?: string; private currentlySelectedWorkspaceFolder: Resource; private readonly autoSelection: IInterpreterAutoSelectionService; @@ -39,7 +45,9 @@ export class InterpreterDisplay implements IInterpreterDisplay { const application = serviceContainer.get(IApplicationShell); const disposableRegistry = serviceContainer.get(IDisposableRegistry); - this.configService = serviceContainer.get(IConfigurationService); + this.interpreterPathExpHelper = serviceContainer.get( + IInterpreterPathProxyService, + ); this.statusBar = application.createStatusBarItem(StatusBarAlignment.Left, 100); this.statusBar.command = 'python.setInterpreter'; @@ -75,7 +83,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { } } private async updateDisplay(workspaceFolder?: Uri) { - const interpreterPath = this.configService.getSettings(workspaceFolder)?.pythonPath; + const interpreterPath = this.interpreterPathExpHelper.get(workspaceFolder); if (!interpreterPath || interpreterPath === 'python') { await this.autoSelection.autoSelectInterpreter(workspaceFolder); // Block on this only if no interpreter selected. } diff --git a/src/test/common/interpreterPathProxyService.unit.test.ts b/src/test/common/interpreterPathProxyService.unit.test.ts new file mode 100644 index 000000000000..1d7116335a66 --- /dev/null +++ b/src/test/common/interpreterPathProxyService.unit.test.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri, WorkspaceConfiguration } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { expect } from 'chai'; +import { InterpreterPathProxyService } from '../../client/common/interpreterPathProxyService'; +import { IExperimentService, IInterpreterPathProxyService, IInterpreterPathService } from '../../client/common/types'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { DeprecatePythonPath } from '../../client/common/experiments/groups'; + +suite('Interpreter Path Proxy Service', async () => { + let interpreterPathProxyService: IInterpreterPathProxyService; + let workspaceService: TypeMoq.IMock; + let experiments: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + const resource = Uri.parse('a'); + const interpreterPath = 'path/to/interpreter'; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + experiments = TypeMoq.Mock.ofType(); + interpreterPathService = TypeMoq.Mock.ofType(); + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ + uri: resource, + name: 'Workspacefolder', + index: 0, + })); + interpreterPathProxyService = new InterpreterPathProxyService( + interpreterPathService.object, + experiments.object, + workspaceService.object, + ); + }); + + test('When in experiment, use interpreter path service to get setting value', () => { + experiments.setup((e) => e.inExperimentSync(DeprecatePythonPath.experiment)).returns(() => true); + interpreterPathService.setup((i) => i.get(resource)).returns(() => interpreterPath); + const value = interpreterPathProxyService.get(resource); + expect(value).to.equal(interpreterPath); + }); + + test('When not in experiment, use workspace service to get setting value', () => { + experiments.setup((e) => e.inExperimentSync(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService.setup((i) => i.getConfiguration('python', resource)).returns(() => workspaceConfig.object); + workspaceConfig.setup((w) => w.get('pythonPath')).returns(() => interpreterPath); + const value = interpreterPathProxyService.get(resource); + expect(value).to.equal(interpreterPath); + }); +}); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index d081758b384e..30b6587ed3fa 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -16,11 +16,10 @@ import { IApplicationShell, IWorkspaceService } from '../../client/common/applic import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import { IFileSystem } from '../../client/common/platform/types'; import { - IConfigurationService, + IInterpreterPathProxyService, IDisposableRegistry, IOutputChannel, IPathUtils, - IPythonSettings, ReadWrite, } from '../../client/common/types'; import { Interpreters } from '../../client/common/utils/localize'; @@ -57,8 +56,7 @@ suite('Interpreters Display', () => { let fileSystem: TypeMoq.IMock; let disposableRegistry: Disposable[]; let statusBar: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let configurationService: TypeMoq.IMock; + let interpreterPathProxyService: TypeMoq.IMock; let interpreterDisplay: IInterpreterDisplay; let interpreterHelper: TypeMoq.IMock; let pathUtils: TypeMoq.IMock; @@ -73,8 +71,7 @@ suite('Interpreters Display', () => { interpreterHelper = TypeMoq.Mock.ofType(); disposableRegistry = []; statusBar = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - configurationService = TypeMoq.Mock.ofType(); + interpreterPathProxyService = TypeMoq.Mock.ofType(); pathUtils = TypeMoq.Mock.ofType(); output = TypeMoq.Mock.ofType(); autoSelection = mock(InterpreterAutoSelectionService); @@ -94,8 +91,8 @@ suite('Interpreters Display', () => { serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configurationService.object); + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterPathProxyService))) + .returns(() => interpreterPathProxyService.object); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) .returns(() => interpreterHelper.object); @@ -215,8 +212,7 @@ suite('Interpreters Display', () => { interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(undefined)); - configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + interpreterPathProxyService.setup((c) => c.get(TypeMoq.It.isAny())).returns(() => pythonPath); fileSystem.setup((f) => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); interpreterHelper .setup((v) => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) From 8253dc807fa5e8df35b0a8a1574a14fc491dcfae Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 20 Jul 2021 11:55:01 -0700 Subject: [PATCH 5/9] Update change log for release. (#16731) --- CHANGELOG.md | 6 +++++- news/1 Enhancements/16461.md | 1 - news/2 Fixes/16723.md | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 news/1 Enhancements/16461.md delete mode 100644 news/2 Fixes/16723.md diff --git a/CHANGELOG.md b/CHANGELOG.md index edc6e3711e08..06094bd75be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## 2021.7.0 (19 July 2021) +## 2021.7.0 (20 July 2021) ### Enhancements +1. Support starting a TensorBoard session with a remote URL hosting log files. + ([#16461](https://github.com/Microsoft/vscode-python/issues/16461)) 1. Sort environments in the selection quickpick by assumed usefulness. ([#16520](https://github.com/Microsoft/vscode-python/issues/16520)) @@ -24,6 +26,8 @@ ([#16607](https://github.com/Microsoft/vscode-python/issues/16607)) 1. The Jupyter Notebook extension will install any missing dependencies using Poetry or Pipenv if those are the selected environments. (thanks [Anthony Shaw](https://github.com/tonybaloney)) ([#16615](https://github.com/Microsoft/vscode-python/issues/16615)) +1. Ensure we block on autoselection when no interpreter is explictly set by user. + ([#16723](https://github.com/Microsoft/vscode-python/issues/16723)) ### Thanks diff --git a/news/1 Enhancements/16461.md b/news/1 Enhancements/16461.md deleted file mode 100644 index 58f5ca182794..000000000000 --- a/news/1 Enhancements/16461.md +++ /dev/null @@ -1 +0,0 @@ -Support starting a TensorBoard session with a remote URL hosting log files. \ No newline at end of file diff --git a/news/2 Fixes/16723.md b/news/2 Fixes/16723.md deleted file mode 100644 index 75b29c7e8244..000000000000 --- a/news/2 Fixes/16723.md +++ /dev/null @@ -1 +0,0 @@ -Ensure we block on autoselection when no interpreter is explictly set by user. From ae979b1769154c9dcc614f249575ffba76d7f278 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Jul 2021 13:04:23 -0700 Subject: [PATCH 6/9] Fix autoselection when opening a python file directly (#16733) * Fix autoselection when opening a python file directly * Update changelog * Add tests --- CHANGELOG.md | 2 ++ src/client/common/configSettings.ts | 11 +++------- .../configSettings.pythonPath.unit.test.ts | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06094bd75be0..4d65bff4a7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ ([#16615](https://github.com/Microsoft/vscode-python/issues/16615)) 1. Ensure we block on autoselection when no interpreter is explictly set by user. ([#16723](https://github.com/Microsoft/vscode-python/issues/16723)) +1. Fix autoselection when opening a python file directly. + ([#16733](https://github.com/Microsoft/vscode-python/issues/16733)) ### Thanks diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 2bc5cfb24dfd..fc06923aba8e 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -679,18 +679,13 @@ export class PythonSettings implements IPythonSettings { const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter( this.workspaceRoot, ); - if (inExperiment) { - if (autoSelectedPythonInterpreter && this.workspaceRoot) { - this.pythonPath = autoSelectedPythonInterpreter.path; + if (autoSelectedPythonInterpreter) { + this.pythonPath = autoSelectedPythonInterpreter.path; + if (this.workspaceRoot) { this.interpreterAutoSelectionService .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) .ignoreErrors(); } - } else if (autoSelectedPythonInterpreter && this.workspaceRoot) { - this.pythonPath = autoSelectedPythonInterpreter.path; - this.interpreterAutoSelectionService - .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) - .ignoreErrors(); } } if (inExperiment && this.pythonPath === DEFAULT_INTERPRETER_SETTING) { diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index bced9e2f8c6d..06a58b65a053 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -6,7 +6,7 @@ import { expect } from 'chai'; import * as path from 'path'; import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { Uri, WorkspaceConfiguration } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; @@ -118,7 +118,24 @@ suite('Python Settings - pythonPath', () => { expect(configSettings.pythonPath).to.be.equal('python'); }); - test("If we don't have a custom python path and we do have an auto selected interpreter, then use it", () => { + test("If a workspace is opened and if we don't have a custom python path but we do have an auto selected interpreter, then use it", () => { + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter = { path: pythonPath } as PythonEnvironment; + const workspaceFolderUri = Uri.file(__dirname); + const selectionService = mock(MockAutoSelectionService); + when(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).thenReturn(interpreter); + when(selectionService.setWorkspaceInterpreter(workspaceFolderUri, anything())).thenResolve(); + configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => 'python') + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + verify(selectionService.setWorkspaceInterpreter(workspaceFolderUri, interpreter)).once(); // Verify we set the autoselected interpreter + }); + test("If no workspace is opened and we don't have a custom python path but we do have an auto selected interpreter, then use it", () => { const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); const interpreter = { path: pythonPath } as PythonEnvironment; const workspaceFolderUri = Uri.file(__dirname); From 0d910c6f7267d65bc575e38663df26f03219db33 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Date: Tue, 20 Jul 2021 13:32:18 -0700 Subject: [PATCH 7/9] Ignore cache when querying for interpreters during auto-selection (#16734) * Ignore cache when getting envs for autoselection * Don't call autoSelectInterpreter twice --- src/client/activation/activationManager.ts | 1 - src/client/interpreter/autoSelection/index.ts | 2 +- .../activation/activationManager.unit.test.ts | 9 --- .../autoSelection/index.unit.test.ts | 77 +++++++++++-------- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index bd7fc45f6517..002a1e60b106 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -61,7 +61,6 @@ export class ExtensionActivationManager implements IExtensionActivationManager { Promise.all(this.singleActivationServices.map((item) => item.activate())), this.activateWorkspace(this.activeResourceService.getActiveResource()), ]); - await this.autoSelection.autoSelectInterpreter(undefined); } @traceDecorators.error('Failed to activate a workspace') diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index 1340c0c4daee..59acef376eea 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -260,7 +260,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio * As such, we can sort interpreters based on what it returns. */ private async autoselectInterpreterWithLocators(resource: Resource): Promise { - const interpreters = await this.interpreterService.getInterpreters(resource); + const interpreters = await this.interpreterService.getInterpreters(resource, { ignoreCache: true }); const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); // When auto-selecting an intepreter for a workspace, we either want to return a local one diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 3daaec52f34b..c0baa2f5b19a 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -456,25 +456,16 @@ suite('Activation Manager', () => { .setup((s) => s.activate()) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - autoSelection - .setup((a) => a.autoSelectInterpreter(undefined)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); when(activeResourceService.getActiveResource()).thenReturn(resource); await managerTest.activate(); assert.ok(initialize.calledOnce); assert.ok(activateWorkspace.calledOnce); singleActivationService.verifyAll(); - autoSelection.verifyAll(); }); test('Throws error if execution fails', async () => { singleActivationService .setup((s) => s.activate()) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - autoSelection - .setup((a) => a.autoSelectInterpreter(undefined)) .returns(() => Promise.reject(new Error('Kaboom'))) .verifiable(typemoq.Times.once()); when(activeResourceService.getActiveResource()).thenReturn(resource); diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index e56db655139b..ac54badba8a5 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -30,7 +30,12 @@ import { IInterpreterAutoSelectionProxyService, } from '../../../client/interpreter/autoSelection/types'; import { EnvironmentTypeComparer } from '../../../client/interpreter/configuration/environmentTypeComparer'; -import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; +import { + GetInterpreterOptions, + IInterpreterHelper, + IInterpreterService, + WorkspacePythonPath, +} from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; @@ -203,25 +208,30 @@ suite('Interpreters - Auto Selection', () => { envPath: path.join(workspacePath, '.venv'), version: { major: 3, minor: 10, patch: 0 }, } as PythonEnvironment; - - when(interpreterService.getInterpreters(resource)).thenResolve([ - { - envType: EnvironmentType.Conda, - envPath: path.join('some', 'conda', 'env'), - version: { major: 3, minor: 7, patch: 2 }, - } as PythonEnvironment, - { - envType: EnvironmentType.System, - envPath: path.join('/', 'usr', 'bin'), - version: { major: 3, minor: 9, patch: 1 }, - } as PythonEnvironment, - localEnv, - ]); + let options: GetInterpreterOptions = {}; + + when(interpreterService.getInterpreters(resource, anything())).thenCall((_, opts) => { + options = opts; + return Promise.resolve([ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment, + localEnv, + ]); + }); await autoSelectionService.autoSelectInterpreter(resource); expect(eventFired).to.deep.equal(true, 'event not fired'); - verify(interpreterService.getInterpreters(resource)).once(); + expect(options).to.deep.equal({ ignoreCache: true }, 'getInterpreters options are different'); + verify(interpreterService.getInterpreters(resource, anything())).once(); verify(state.updateValue(localEnv)).once(); }); @@ -231,25 +241,30 @@ suite('Interpreters - Auto Selection', () => { envPath: path.join('/', 'usr', 'bin'), version: { major: 3, minor: 9, patch: 1 }, } as PythonEnvironment; - - when(interpreterService.getInterpreters(resource)).thenResolve([ - { - envType: EnvironmentType.Conda, - envPath: path.join('some', 'conda', 'env'), - version: { major: 3, minor: 7, patch: 2 }, - } as PythonEnvironment, - systemEnv, - { - envType: EnvironmentType.Pipenv, - envPath: path.join('some', 'pipenv', 'env'), - version: { major: 3, minor: 10, patch: 0 }, - } as PythonEnvironment, - ]); + let options: GetInterpreterOptions = {}; + + when(interpreterService.getInterpreters(resource, anything())).thenCall((_, opts) => { + options = opts; + return Promise.resolve([ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + systemEnv, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + }); await autoSelectionService.autoSelectInterpreter(resource); expect(eventFired).to.deep.equal(true, 'event not fired'); - verify(interpreterService.getInterpreters(resource)).once(); + expect(options).to.deep.equal({ ignoreCache: true }, 'getInterpreters options are different'); + verify(interpreterService.getInterpreters(resource, anything())).once(); verify(state.updateValue(systemEnv)).once(); }); }); From 178eaa871c9b2622704aace08e9c712fcedc2752 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 21 Jul 2021 13:01:55 -0700 Subject: [PATCH 8/9] Update debugger via point release (#16746) * Update version * Update change log * Update wheels to 3.9 (#16745) --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- pythonFiles/install_debugpy.py | 2 +- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d65bff4a7a2..65092d05e1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +## 2021.7.1 (21 July 2021) + +### Enhancements + +1. Update `debugpy` to the latest version. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + ## 2021.7.0 (20 July 2021) ### Enhancements diff --git a/package-lock.json b/package-lock.json index 8c81bb9aa786..729b55619aa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "python", - "version": "2021.7.0", + "version": "2021.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 853a41ba84fd..29893bdd498a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2021.7.0", + "version": "2021.7.1", "featureFlags": { "usingNewInterpreterStorage": true }, diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py index e9361a6aeb3e..a74a860cbbf6 100644 --- a/pythonFiles/install_debugpy.py +++ b/pythonFiles/install_debugpy.py @@ -9,7 +9,7 @@ EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" -DEBUGGER_PYTHON_VERSIONS = ("cp38",) +DEBUGGER_PYTHON_VERSIONS = ("cp39",) def _contains(s, parts=()): From f79a7e4f7abcdd83ba0b2de2d870bf7030b55149 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 23 Jul 2021 13:17:17 -0700 Subject: [PATCH 9/9] Point release with debugpy fix (#16776) --- CHANGELOG.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65092d05e1be..5bd92e910f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +## 2021.7.2 (23 July 2021) + +### Enhancements + +1. Update `debugpy` with fix for https://github.com/microsoft/debugpy/issues/669. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [jedi-language-server](https://pypi.org/project/jedi-language-server/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + ## 2021.7.1 (21 July 2021) ### Enhancements diff --git a/package-lock.json b/package-lock.json index 729b55619aa8..3b51c5642264 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "python", - "version": "2021.7.1", + "version": "2021.7.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 29893bdd498a..f43ad91590c3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2021.7.1", + "version": "2021.7.2", "featureFlags": { "usingNewInterpreterStorage": true },