diff --git a/package.json b/package.json index af2c2b33d7e0..9d1cf544b692 100644 --- a/package.json +++ b/package.json @@ -1465,10 +1465,10 @@ ], "languageModelTools": [ { - "name": "get_python_environment", - "displayName": "Get Python Environment Information", - "userDescription": "%python.languageModelTools.python_environment.userDescription%", - "modelDescription": "Provides details about the Python environment for a specified file or workspace, including environment type, Python version, run command, and installed packages with their versions. Use this tool to determine the correct command for executing Python code in this workspace.", + "name": "get_python_environment_info", + "displayName": "Get Python Environment Info", + "userDescription": "%python.languageModelTools.get_python_environment_info.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ "ms-python.python" @@ -1483,11 +1483,53 @@ } }, "description": "The path to the Python file or workspace to get the environment information for.", - "required": [ - "resourcePath" - ] + "required": [] + } + }, + { + "name": "get_python_executable", + "displayName": "Get Python Executable", + "userDescription": "%python.languageModelTools.get_python_executable.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`.", + "toolReferenceName": "pythonExecutableCommand", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", + "required": [] + } + }, + { + "name": "list_python_packages", + "displayName": "List Python Packages", + "userDescription": "%python.languageModelTools.list_python_packages.userDescription%", + "modelDescription": "This tool will retrieve the list of all installed packages installed in a Python Environment for the specified file or workspace. ALWAYS use this tool instead of executing Python command in the terminal to fetch the list of installed packages. WARNING: Packages installed can change over time, hence the list of packages returned by this tool may not be accurate. Use this tool to get the list of installed packages in a Python environment.", + "toolReferenceName": "listPythonPackages", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to list the packages. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", + "required": [] }, - "when": "!pythonEnvExtensionInstalled" + "when": "false" }, { "name": "install_python_package", @@ -1512,12 +1554,11 @@ }, "resourcePath": { "type": "string", - "description": "The path to the Python file or workspace to get the environment information for." + "description": "The path to the Python file or workspace into which the packages are installed. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." } }, "required": [ - "packageList", - "resourcePath" + "packageList" ] }, "when": "!pythonEnvExtensionInstalled" diff --git a/package.nls.json b/package.nls.json index 22e5cf4fd8ad..a73deb3554da 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,7 +1,9 @@ { "python.command.python.startTerminalREPL.title": "Start Terminal REPL", - "python.languageModelTools.python_environment.userDescription": "Get Python environment info for a file or path, including version, packages, and the command to run it.", - "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in the given workspace.", + "python.languageModelTools.get_python_environment_info.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", + "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.list_python_packages.userDescription": "Get a list of all installed packages in a Python Environment.", "python.command.python.startNativeREPL.title": "Start Native Python REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts new file mode 100644 index 000000000000..af4ab214419a --- /dev/null +++ b/src/client/chat/getExecutableTool.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { getEnvDisplayName, getEnvironmentDetails, raceCancellationError } from './utils'; +import { resolveFilePath } from './utils'; +import { traceError } from '../logging'; +import { ITerminalHelper } from '../common/terminal/types'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; + +export interface IResourceReference { + resourcePath?: string; +} + +export class GetExecutableTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_executable'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + try { + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + traceError('Error while getting environment information', error); + const errorMessage: string = `An error occurred while fetching environment information: ${error}`; + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); + } + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Fetching Python executable information for {0}', envName) + : l10n.t('Fetching Python executable information'), + }; + } +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 43bd254efa28..ef200239af9a 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -11,38 +11,27 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - Uri, } from 'vscode'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { raceCancellationError } from './utils'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { getEnvironmentDetails, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; -import { parsePipList } from './pipListUtils'; -import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; -import { traceError } from '../logging'; +import { getPythonPackagesResponse } from './listPackagesTool'; +import { ITerminalHelper } from '../common/terminal/types'; export interface IResourceReference { - resourcePath: string; + resourcePath?: string; } -interface EnvironmentInfo { - type: string; // e.g. conda, venv, virtualenv, sys - version: string; - runCommand: string; - packages: string[] | string; //include versions too -} - -/** - * A tool to get the information about the Python environment. - */ export class GetEnvironmentInfoTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly pythonExecFactory: IPythonExecutionFactory; private readonly processServiceFactory: IProcessServiceFactory; - public static readonly toolName = 'get_python_environment'; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_environment_info'; constructor( private readonly api: PythonExtension['environments'], private readonly serviceContainer: IServiceContainer, @@ -53,6 +42,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(IPythonExecutionFactory); this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); } /** * Invokes the tool to get the information about the Python environment. @@ -66,53 +56,37 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { const resourcePath = resolveFilePath(options.input.resourcePath); - // environment info set to default values - const envInfo: EnvironmentInfo = { - type: 'no type found', - version: 'no version found', - packages: 'no packages found', - runCommand: 'no run command found', - }; - try { // environment const envPath = this.api.getActiveEnvironmentPath(resourcePath); const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); } - const cmd = await raceCancellationError( - this.terminalExecutionService.getExecutableInfo(resourcePath), + const packages = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, token, ); - const executable = cmd.pythonExecutable; - envInfo.runCommand = cmd.args.length > 0 ? `${cmd.command} ${cmd.args.join(' ')}` : executable; - envInfo.version = environment.version.sysVersion; - const isConda = (environment.environment?.type || '').toLowerCase() === 'conda'; - envInfo.packages = isConda - ? await raceCancellationError( - listCondaPackages( - this.pythonExecFactory, - environment, - resourcePath, - await raceCancellationError(this.processServiceFactory.create(resourcePath), token), - ), - token, - ) - : await raceCancellationError(listPipPackages(this.pythonExecFactory, resourcePath), token); + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + packages, + token, + ); - // format and return - return new LanguageModelToolResult([BuildEnvironmentInfoContent(envInfo)]); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); } catch (error) { if (error instanceof CancellationError) { throw error; } const errorMessage: string = `An error occurred while fetching environment information: ${error}`; - const partialContent = BuildEnvironmentInfoContent(envInfo); - return new LanguageModelToolResult([ - new LanguageModelTextPart(`${errorMessage}\n\n${partialContent.value}`), - ]); + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); } } @@ -125,75 +99,3 @@ export class GetEnvironmentInfoTool implements LanguageModelTool or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. - "packages": ${JSON.stringify(Array.isArray(envInfo.packages) ? envInfo.packages : envInfo.packages, null, 2)} -}`; - - return new LanguageModelTextPart(content); -} - -async function listPipPackages(execFactory: IPythonExecutionFactory, resource: Uri) { - // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) - // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. - const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); - const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); - return parsePipList(output.stdout).map((pkg) => (pkg.version ? `${pkg.name} (${pkg.version})` : pkg.name)); -} - -async function listCondaPackages( - execFactory: IPythonExecutionFactory, - env: ResolvedEnvironment, - resource: Uri, - processService: IProcessService, -) { - const conda = await Conda.getConda(); - if (!conda) { - traceError('Conda is not installed, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - if (!env.executable.uri) { - traceError('Conda environment executable not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); - if (!condaEnv) { - traceError('Conda environment not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); - if (!cmd) { - traceError('Conda list command not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); - if (!output.stdout) { - traceError('Unable to get conda packages, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); - const packages: string[] = []; - content.forEach((l) => { - const parts = l.split(' ').filter((p) => p.length > 0); - if (parts.length === 3) { - packages.push(`${parts[0]} (${parts[1]})`); - } - }); - return packages; -} diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts index 918774911107..d51f0d1ade64 100644 --- a/src/client/chat/index.ts +++ b/src/client/chat/index.ts @@ -4,12 +4,14 @@ import { commands, extensions, lm } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; -import { GetEnvironmentInfoTool } from './getPythonEnvTool'; import { InstallPackagesTool } from './installPackagesTool'; import { IExtensionContext } from '../common/types'; import { DisposableStore } from '../common/utils/resourceLifecycle'; import { ENVS_EXTENSION_ID } from '../envExt/api.internal'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { ListPythonPackagesTool } from './listPackagesTool'; +import { GetExecutableTool } from './getExecutableTool'; +import { GetEnvironmentInfoTool } from './getPythonEnvTool'; export function registerTools( context: IExtensionContext, @@ -28,6 +30,18 @@ export function registerTools( ourTools.add( lm.registerTool(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(environmentsApi, serviceContainer)), ); + ourTools.add( + lm.registerTool( + GetExecutableTool.toolName, + new GetExecutableTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + ourTools.add( + lm.registerTool( + ListPythonPackagesTool.toolName, + new ListPythonPackagesTool(environmentsApi, serviceContainer, discoverApi), + ), + ); ourTools.add( lm.registerTool( InstallPackagesTool.toolName, diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index d0bfc3ce65de..a430525e1018 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -11,18 +11,17 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - Uri, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; -import { raceCancellationError } from './utils'; +import { getEnvDisplayName, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; export interface IInstallPackageArgs { - resourcePath: string; + resourcePath?: string; packageList: string[]; } @@ -52,7 +51,7 @@ export class InstallPackagesTool implements LanguageModelTool(IModuleInstaller); @@ -120,13 +119,3 @@ export class InstallPackagesTool implements LanguageModelTool { + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + public static readonly toolName = 'list_python_packages'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + } + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + + const message = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + return new LanguageModelToolResult([ + new LanguageModelTextPart(`An error occurred while fetching environment information: ${error}`), + ]); + } + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Listing packages in {0}', envName) + : l10n.t('Fetching Python environment information'), + }; + } +} + +export async function getPythonPackagesResponse( + environment: ResolvedEnvironment, + pythonExecFactory: IPythonExecutionFactory, + processServiceFactory: IProcessServiceFactory, + resourcePath: Uri | undefined, + token: CancellationToken, +): Promise { + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return 'No packages found'; + } + + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return response.join('\n'); +} + +async function listPipPackages( + execFactory: IPythonExecutionFactory, + resource: Uri | undefined, +): Promise<[string, string][]> { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri | undefined, + processService: IProcessService, +): Promise<[string, string][]> { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: [string, string][] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length >= 3) { + packages.push([parts[0], parts[1]]); + } + }); + return packages; +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 05d92337df43..6206e01ea655 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -1,9 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationError, CancellationToken, Uri } from 'vscode'; +import { CancellationError, CancellationToken, Uri, workspace } from 'vscode'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; -export function resolveFilePath(filepath: string): Uri { +export function resolveFilePath(filepath?: string): Uri | undefined { + if (!filepath) { + return workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined; + } // starts with a scheme try { return Uri.parse(filepath); @@ -25,3 +33,86 @@ export function raceCancellationError(promise: Promise, token: Cancellatio promise.then(resolve, reject).finally(() => ref.dispose()); }); } + +export async function getEnvDisplayName( + discovery: IDiscoveryAPI, + resource: Uri | undefined, + api: PythonExtension['environments'], +) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} + +export function isCondaEnv(env: ResolvedEnvironment) { + return (env.environment?.type || '').toLowerCase() === 'conda'; +} + +export async function getEnvironmentDetails( + resourcePath: Uri | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + packages: string | undefined, + token: CancellationToken, +): Promise { + // environment + const envPath = api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + const runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); + + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${environment.environment?.type || 'unknown'}`, + `2. Version: ${environment.version.sysVersion || 'unknown'}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + packages ? `4. ${packages}` : '', + ]; + return message.join('\n'); +} + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} diff --git a/src/test/common.ts b/src/test/common.ts index b6e352b9a3e8..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -62,7 +62,7 @@ export async function updateSetting( configTarget: ConfigurationTarget, ) { const vscode = require('vscode') as typeof import('vscode'); - const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' } || null); + const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' }); const currentValue = settings.inspect(setting); if ( currentValue !== undefined && diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts index 762c23d86c8e..5c851b8071f3 100644 --- a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -17,6 +17,7 @@ suite('Python Path Settings Updater', () => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); interpreterPathService = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index ceee7f54f447..cdf0d00c5dc4 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -16,6 +16,7 @@ import { UnittestTestExecutionAdapter } from '../../../client/testing/testContro import { MockChildProcess } from '../../mocks/mockChildProcess'; import * as util from '../../../client/testing/testController/common/utils'; import * as extapi from '../../../client/envExt/api.internal'; +import { noop } from '../../core'; const adapters: Array = ['pytest', 'unittest']; @@ -36,6 +37,11 @@ suite('Execution Flow Run Adapters', () => { let useEnvExtensionStub: sinon.SinonStub; setup(() => { + const proc = typeMoq.Mock.ofType(); + proc.setup((p) => p.on).returns(() => noop as any); + proc.setup((p) => p.stdout).returns(() => null); + proc.setup((p) => p.stderr).returns(() => null); + mockProc = proc.object; useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); // general vars