diff --git a/extension/resources/walkthrough/install-dvc.md b/extension/resources/walkthrough/install-dvc.md index 1a2caff988..f881c531ce 100644 --- a/extension/resources/walkthrough/install-dvc.md +++ b/extension/resources/walkthrough/install-dvc.md @@ -22,3 +22,6 @@ right environment:

DVC Setup Wizard

+ +> **Note**: The correct Python interpreter must be set for the current workspace +> when relying on the Python extension for auto environment activation. diff --git a/extension/src/extensions/python.ts b/extension/src/extensions/python.ts index 7c72c56afb..7c310345d1 100644 --- a/extension/src/extensions/python.ts +++ b/extension/src/extensions/python.ts @@ -1,4 +1,4 @@ -import { Event, Uri } from 'vscode' +import { commands, Event, Uri } from 'vscode' import { executeProcess } from '../processExecution' import { getExtensionAPI, isInstalled } from '../vscode/extensions' @@ -54,3 +54,7 @@ export const getOnDidChangePythonExecutionDetails = async () => { } export const isPythonExtensionInstalled = () => isInstalled(PYTHON_EXTENSION_ID) + +export const selectPythonInterpreter = () => { + commands.executeCommand('python.setInterpreter') +} diff --git a/extension/src/setup.test.ts b/extension/src/setup.test.ts index edfc6fb86f..987b1351a1 100644 --- a/extension/src/setup.test.ts +++ b/extension/src/setup.test.ts @@ -1,5 +1,5 @@ -import { resolve } from 'path' -import { extensions, Extension } from 'vscode' +import { join, resolve } from 'path' +import { extensions, Extension, commands } from 'vscode' import { setup, setupWorkspace } from './setup' import { flushPromises } from './test/util/jest' import { @@ -16,6 +16,8 @@ import { import { getFirstWorkspaceFolder } from './vscode/workspaceFolders' import { Toast } from './vscode/toast' import { Response } from './vscode/response' +import { VscodePython } from './extensions/python' +import { executeProcess } from './processExecution' jest.mock('vscode') jest.mock('./vscode/config') @@ -23,8 +25,34 @@ jest.mock('./vscode/resourcePicker') jest.mock('./vscode/quickPick') jest.mock('./vscode/toast') jest.mock('./vscode/workspaceFolders') +jest.mock('./processExecution') const mockedExtensions = jest.mocked(extensions) +const mockedCommands = jest.mocked(commands) +const mockedExecuteCommand = jest.fn() +mockedCommands.executeCommand = mockedExecuteCommand + +const mockedExecuteProcess = jest.mocked(executeProcess) + +const mockedGetExtension = jest.fn() +mockedExtensions.getExtension = mockedGetExtension + +const mockedReady = jest.fn() + +const mockedSettings = { + getExecutionDetails: () => ({ + execCommand: [join('some', 'bin', 'path')] + }) +} + +const mockedVscodePythonAPI = { + ready: mockedReady, + settings: mockedSettings +} as unknown as VscodePython + +const mockedVscodePython = { + activate: () => Promise.resolve(mockedVscodePythonAPI) +} const mockedCanRunCli = jest.fn() const mockedHasRoots = jest.fn() @@ -256,8 +284,14 @@ describe('setup', () => { mockedHasRoots.mockReturnValueOnce(true) mockedCanRunCli.mockRejectedValueOnce(new Error('command not found: dvc')) mockedWarnWithOptions.mockResolvedValueOnce(undefined) + mockedExecuteProcess.mockImplementation(({ executable }) => + Promise.resolve(executable) + ) + mockedReady.mockResolvedValue(true) + mockedGetExtension.mockReturnValue(mockedVscodePython) await setup(extension) + await flushPromises() expect(mockedSetRoots).toBeCalledTimes(1) expect(mockedGetConfigValue).toBeCalledTimes(1) expect(mockedWarnWithOptions).toBeCalledTimes(1) @@ -270,6 +304,11 @@ describe('setup', () => { mockedHasRoots.mockReturnValueOnce(true) mockedCanRunCli.mockRejectedValueOnce(new Error('command not found: dvc')) mockedWarnWithOptions.mockResolvedValueOnce(Response.SETUP_WORKSPACE) + mockedExecuteProcess.mockImplementation(({ executable }) => + Promise.resolve(executable) + ) + mockedReady.mockResolvedValue(true) + mockedGetExtension.mockReturnValue(mockedVscodePython) await setup(extension) await flushPromises() @@ -277,6 +316,30 @@ describe('setup', () => { expect(mockedGetConfigValue).toBeCalledTimes(1) expect(mockedWarnWithOptions).toBeCalledTimes(1) expect(mockedSetupWorkspace).toBeCalledTimes(1) + expect(mockedExecuteCommand).not.toBeCalled() + expect(mockedSetUserConfigValue).not.toBeCalled() + expect(mockedResetMembers).toBeCalledTimes(1) + expect(mockedInitialize).not.toBeCalled() + }) + + it('should try to select the python interpreter if the workspace contains a DVC project, the cli cannot be found and the user decides to select the python interpreter', async () => { + mockedGetFirstWorkspaceFolder.mockReturnValueOnce(mockedCwd) + mockedHasRoots.mockReturnValueOnce(true) + mockedCanRunCli.mockRejectedValueOnce(new Error('command not found: dvc')) + mockedWarnWithOptions.mockResolvedValueOnce(Response.SELECT_INTERPRETER) + mockedExecuteProcess.mockImplementation(({ executable }) => + Promise.resolve(executable) + ) + mockedReady.mockResolvedValue(true) + mockedGetExtension.mockReturnValue(mockedVscodePython) + + await setup(extension) + await flushPromises() + expect(mockedSetRoots).toBeCalledTimes(1) + expect(mockedGetConfigValue).toBeCalledTimes(1) + expect(mockedWarnWithOptions).toBeCalledTimes(1) + expect(mockedSetupWorkspace).toBeCalledTimes(0) + expect(mockedExecuteCommand).toBeCalledTimes(1) expect(mockedSetUserConfigValue).not.toBeCalled() expect(mockedResetMembers).toBeCalledTimes(1) expect(mockedInitialize).not.toBeCalled() @@ -287,6 +350,11 @@ describe('setup', () => { mockedHasRoots.mockReturnValueOnce(true) mockedCanRunCli.mockRejectedValueOnce(new Error('command not found: dvc')) mockedWarnWithOptions.mockResolvedValueOnce(Response.NEVER) + mockedExecuteProcess.mockImplementation(({ executable }) => + Promise.resolve(executable) + ) + mockedReady.mockResolvedValue(true) + mockedGetExtension.mockReturnValue(mockedVscodePython) await setup(extension) await flushPromises() @@ -294,6 +362,7 @@ describe('setup', () => { expect(mockedGetConfigValue).toBeCalledTimes(1) expect(mockedWarnWithOptions).toBeCalledTimes(1) expect(mockedSetupWorkspace).not.toBeCalled() + expect(mockedExecuteCommand).not.toBeCalled() expect(mockedSetUserConfigValue).toBeCalledTimes(1) expect(mockedResetMembers).toBeCalledTimes(1) expect(mockedInitialize).not.toBeCalled() diff --git a/extension/src/setup.ts b/extension/src/setup.ts index e6c998476e..638f837915 100644 --- a/extension/src/setup.ts +++ b/extension/src/setup.ts @@ -15,7 +15,11 @@ import { getFirstWorkspaceFolder } from './vscode/workspaceFolders' import { Response } from './vscode/response' import { getSelectTitle, Title } from './vscode/title' import { Toast } from './vscode/toast' -import { isPythonExtensionInstalled } from './extensions/python' +import { + getPythonBinPath, + isPythonExtensionInstalled, + selectPythonInterpreter +} from './extensions/python' const setConfigPath = async ( option: ConfigKey, @@ -159,22 +163,49 @@ export const setupWorkspace = async (): Promise => { return pickCliPath() } +const getToastText = async ( + isPythonExtensionInstalled: boolean +): Promise => { + const text = 'An error was thrown when trying to access the CLI.' + if (!isPythonExtensionInstalled) { + return text + } + const binPath = await getPythonBinPath() + + return ( + text + + ` For auto Python environment activation ensure the correct interpreter is set. Active Python interpreter: ${binPath}. ` + ) +} + +const getToastOptions = (isPythonExtensionInstalled: boolean): Response[] => { + return isPythonExtensionInstalled + ? [Response.SETUP_WORKSPACE, Response.SELECT_INTERPRETER, Response.NEVER] + : [Response.SETUP_WORKSPACE, Response.NEVER] +} + const warnUserCLIInaccessible = async ( extension: IExtension ): Promise => { if (getConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE)) { return } + + const isMsPythonInstalled = isPythonExtensionInstalled() + const warningText = await getToastText(isMsPythonInstalled) + const response = await Toast.warnWithOptions( - 'An error was thrown when trying to access the CLI.', - Response.SETUP_WORKSPACE, - Response.NEVER + warningText, + ...getToastOptions(isMsPythonInstalled) ) - if (response === Response.SETUP_WORKSPACE) { - extension.setupWorkspace() - } - if (response === Response.NEVER) { - setUserConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE, true) + + switch (response) { + case Response.SELECT_INTERPRETER: + return selectPythonInterpreter() + case Response.SETUP_WORKSPACE: + return extension.setupWorkspace() + case Response.NEVER: + return setUserConfigValue(ConfigKey.DO_NOT_SHOW_CLI_UNAVAILABLE, true) } } diff --git a/extension/src/vscode/response.ts b/extension/src/vscode/response.ts index 2769d77327..37b2b9da1d 100644 --- a/extension/src/vscode/response.ts +++ b/extension/src/vscode/response.ts @@ -7,6 +7,7 @@ export enum Response { NEVER = "Don't Show Again", NO = 'No', SELECT_MOST_RECENT = 'Select Most Recent', + SELECT_INTERPRETER = 'Select Python Interpreter', SETUP_WORKSPACE = 'Setup The Workspace', SHOW = 'Show', TURN_OFF = 'Turn Off',