Skip to content

Add support for detection and selection of conda environments lacking a python interpreter #18427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9ce2893
Support conda environments without a python executable
Feb 3, 2022
d311863
Fix interpreter display
Feb 3, 2022
5fb3f7d
Fix areSameEnv
Feb 3, 2022
aaaaa46
Add support to select env folders as interpreterPaths
Feb 3, 2022
05e50b1
Allow selecting such environments
Feb 3, 2022
56d7bb4
Handle resolving environment path
Feb 4, 2022
6c98d94
Document resolver works for both env path and executable
Feb 4, 2022
0dc5a68
Fxi bug
Feb 4, 2022
4856da1
Simplify conda.ts
Feb 4, 2022
44ae604
Define identifiers for an environment
Feb 7, 2022
e0d8744
Fix getCondaEnvironment
Feb 8, 2022
ebb5206
Introduce an id property to environments
Feb 10, 2022
ef0008e
Update proposed discovery API to handle environments
Feb 10, 2022
1aac36e
Fix bug with interpreter display
Feb 10, 2022
2afd514
Normalize path passed in resolveEnv
Feb 11, 2022
e058b7a
Update environment details API
Feb 11, 2022
50ed39d
Dont use pythonPath for getting active interpreter for activation com…
Feb 14, 2022
da25d60
Support conda activation
Feb 14, 2022
0ca6dd0
Support ${command:python.interpreterPath} with this envs
Feb 14, 2022
6c35c5b
Fix getActiveItem
Feb 14, 2022
80fda87
Add comment justifying using `.pythonPath` for middleware
Feb 14, 2022
b1fcc8e
Fix shebang codelens
Feb 14, 2022
468ee06
Fix startup telemetry
Feb 14, 2022
07802fb
Merge branch 'main' of https://github.com/microsoft/vscode-python int…
Mar 9, 2022
fc33a43
Do not support pip installer for such environments
Mar 9, 2022
d5c0086
Trigger discovery once installation is finished for such environments
Mar 10, 2022
0fae72b
Automatically install python into environment once it is selected
Mar 11, 2022
26bff7c
Add telemetry for new interpreters discovered
Mar 11, 2022
54aaccf
Add telemtry if such an env is selected
Mar 11, 2022
38c5be8
Merge branch 'main' of https://github.com/microsoft/vscode-python int…
Mar 11, 2022
629ef20
Fix bugs and cache installer
Mar 15, 2022
7dfa521
Fix compile errors, tests, and add tests
Mar 15, 2022
9b09188
Fix some tests
Mar 15, 2022
b0cf7be
Ignore id when comparing envs
Mar 15, 2022
21189bd
Phew, fixed terrible tests in module installer
Mar 16, 2022
0ec2461
Fix more tests
Mar 16, 2022
657b8fc
MOre
Mar 16, 2022
fbe1d39
Skip resolver tests on linux
Mar 17, 2022
104be45
Fix tests
Mar 17, 2022
1f508dc
If environment doesn't contain python do not support pip installer
Mar 17, 2022
1220841
Skip module install tests for python as it is not a module
Mar 17, 2022
592d46d
News entry
Mar 17, 2022
6f10dcb
Merge branch 'main' of https://github.com/microsoft/vscode-python int…
Mar 17, 2022
bc27abd
Remove unnecssary commnet
Mar 18, 2022
51d6eb0
Fix bug introduced by merges
Mar 18, 2022
7763572
Code reviews
Mar 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/18357.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for detection and selection of conda environments lacking a python interpreter.
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"Pylance.pylanceRevertToJedi": "Revert to Jedi",
"Experiments.inGroup": "Experiment '{0}' is active",
"Experiments.optedOutOf": "Experiment '{0}' is inactive",
"Interpreters.installingPython": "Installing Python into Environment...",
"Interpreters.clearAtWorkspace": "Clear at workspace level",
"Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters",
"Interpreters.entireWorkspace": "Select at workspace level",
Expand Down
2 changes: 2 additions & 0 deletions src/client/activation/languageClientMiddlewareBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export class LanguageClientMiddlewareBase implements Middleware {
const uri = item.scopeUri ? Uri.parse(item.scopeUri) : undefined;
// For backwards compatibility, set python.pythonPath to the configured
// value as though it were in the user's settings.json file.
// As this is for backwards compatibility, `ConfigService.pythonPath`
// can be considered as active interpreter path.
settings[i].pythonPath = configService.getSettings(uri).pythonPath;

const env = await envService.getEnvironmentVariables(uri);
Expand Down
75 changes: 45 additions & 30 deletions src/client/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { Event, Uri } from 'vscode';
import { Resource } from './common/types';
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
import { EnvPathType, PythonEnvKind } from './pythonEnvironments/base/info';

/*
* Do not introduce any breaking changes to this API.
Expand Down Expand Up @@ -87,28 +88,39 @@ export interface IExtensionApi {
};
}

export interface InterpreterDetailsOptions {
export interface EnvironmentDetailsOptions {
useCache: boolean;
}

export interface InterpreterDetails {
path: string;
export interface EnvironmentDetails {
interpreterPath: string;
envFolderPath?: string;
version: string[];
environmentType: string[];
environmentType: PythonEnvKind[];
metadata: Record<string, unknown>;
}

export interface InterpretersChangedParams {
export interface EnvironmentsChangedParams {
/**
* Path to environment folder or path to interpreter that uniquely identifies an environment.
* Virtual environments lacking an interpreter are identified by environment folder paths,
* whereas other envs can be identified using interpreter path.
*/
path?: string;
type: 'add' | 'remove' | 'update' | 'clear-all';
}

export interface ActiveInterpreterChangedParams {
interpreterPath?: string;
export interface ActiveEnvironmentChangedParams {
/**
* Path to environment folder or path to interpreter that uniquely identifies an environment.
* Virtual environments lacking an interpreter are identified by environment folder paths,
* whereas other envs can be identified using interpreter path.
*/
path: string;
resource?: Uri;
}

export interface RefreshInterpretersOptions {
export interface RefreshEnvironmentsOptions {
clearCache?: boolean;
}

Expand All @@ -122,57 +134,60 @@ export interface IProposedExtensionAPI {
* returns what ever is set for the workspace.
* @param resource : Uri of a file or workspace
*/
getActiveInterpreterPath(resource?: Resource): Promise<string | undefined>;
getActiveEnvironmentPath(resource?: Resource): Promise<EnvPathType | undefined>;
/**
* Returns details for the given interpreter. Details such as absolute interpreter path,
* version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under
* metadata field.
* @param interpreterPath : Path of the interpreter whose details you need.
* @param path : Path to environment folder or path to interpreter whose details you need.
* @param options : [optional]
* * useCache : When true, cache is checked first for any data, returns even if there
* is partial data.
*/
getInterpreterDetails(
interpreterPath: string,
options?: InterpreterDetailsOptions,
): Promise<InterpreterDetails | undefined>;
getEnvironmentDetails(
path: string,
options?: EnvironmentDetailsOptions,
): Promise<EnvironmentDetails | undefined>;
/**
* Returns paths to interpreters found by the extension at the time of calling. This API
* will *not* trigger a refresh. If a refresh is going on it will *not* wait for the refresh
* to finish. This will return what is known so far. To get complete list `await` on promise
* returned by `getRefreshPromise()`.
* Returns paths to environments that uniquely identifies an environment found by the extension
* at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it
* will *not* wait for the refresh to finish. This will return what is known so far. To get
* complete list `await` on promise returned by `getRefreshPromise()`.
*
* Virtual environments lacking an interpreter are identified by environment folder paths,
* whereas other envs can be identified using interpreter path.
*/
getInterpreterPaths(): Promise<string[] | undefined>;
getEnvironmentPaths(): Promise<EnvPathType[] | undefined>;
/**
* Sets the active interpreter path for the python extension. Configuration target will
* always be the workspace.
* @param interpreterPath : Interpreter path to set for a given workspace.
* Sets the active environment path for the python extension. Configuration target will
* always be the workspace folder.
* @param path : Interpreter path to set for a given workspace.
* @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace
* folder.
*/
setActiveInterpreter(interpreterPath: string, resource?: Resource): Promise<void>;
setActiveEnvironment(path: string, resource?: Resource): Promise<void>;
/**
* This API will re-trigger environment discovery. Extensions can wait on the returned
* promise to get the updated interpreters list. If there is a refresh already going on
* promise to get the updated environment list. If there is a refresh already going on
* then it returns the promise for that refresh.
* @param options : [optional]
* * clearCache : When true, this will clear the cache before interpreter refresh
* is triggered.
*/
refreshInterpreters(options?: RefreshInterpretersOptions): Promise<string[] | undefined>;
refreshEnvironment(options?: RefreshEnvironmentsOptions): Promise<EnvPathType[] | undefined>;
/**
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
* refreshes going on.
*/
getRefreshPromise(): Promise<void> | undefined;
/**
* This event is triggered when the known interpreters list changes, like when a interpreter
* is found, existing interpreter is removed, or some details changed on an interpreter.
* This event is triggered when the known environment list changes, like when a environment
* is found, existing environment is removed, or some details changed on an environment.
*/
onDidInterpretersChanged: Event<InterpretersChangedParams[]>;
onDidEnvironmentsChanged: Event<EnvironmentsChangedParams[]>;
/**
* This event is triggered when the active interpreter changes.
* This event is triggered when the active environment changes.
*/
onDidActiveInterpreterChanged: Event<ActiveInterpreterChangedParams>;
onDidActiveEnvironmentChanged: Event<ActiveEnvironmentChangedParams>;
};
}
4 changes: 2 additions & 2 deletions src/client/common/installer/condaInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class CondaInstaller extends ModuleInstaller {

const pythonPath = isResource(resource)
? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath
: resource.path;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.path is no longer unique.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.path is no longer unique.

: resource.id ?? '';
const condaLocatorService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter);
const info = await condaLocatorService.getCondaEnvironment(pythonPath);
const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install'];
Expand Down Expand Up @@ -127,7 +127,7 @@ export class CondaInstaller extends ModuleInstaller {
const condaService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter);
const pythonPath = isResource(resource)
? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath
: resource.path;
: resource.id ?? '';
return condaService.isCondaEnvironment(pythonPath);
}
}
76 changes: 59 additions & 17 deletions src/client/common/installer/moduleInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@ import { wrapCancellationTokens } from '../cancellation';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { IFileSystem } from '../platform/types';
import * as internalPython from '../process/internal/python';
import { IProcessServiceFactory } from '../process/types';
import { ITerminalServiceFactory, TerminalCreationOptions } from '../terminal/types';
import { ExecutionInfo, IConfigurationService, IOutputChannel, Product } from '../types';
import { Products } from '../utils/localize';
import { isResource } from '../utils/misc';
import { ProductNames } from './productNames';
import { IModuleInstaller, InterpreterUri, ModuleInstallFlags } from './types';
import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types';

@injectable()
export abstract class ModuleInstaller implements IModuleInstaller {
public abstract get priority(): number;

public abstract get name(): string;

public abstract get displayName(): string;

public abstract get type(): ModuleInstallerType;

constructor(protected serviceContainer: IServiceContainer) {}
Expand All @@ -36,24 +40,18 @@ export abstract class ModuleInstaller implements IModuleInstaller {
resource?: InterpreterUri,
cancel?: CancellationToken,
flags?: ModuleInstallFlags,
options?: InstallOptions,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add support for installing modules as a process. Earlier we just sent the command in terminal.

): Promise<void> {
const shouldExecuteInTerminal = !options?.installAsProcess;
const name =
typeof productOrModuleName == 'string'
typeof productOrModuleName === 'string'
? productOrModuleName
: translateProductToModule(productOrModuleName);
const productName = typeof productOrModuleName === 'string' ? name : ProductNames.get(productOrModuleName);
sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { installer: this.displayName, productName });
const uri = isResource(resource) ? resource : undefined;
const options: TerminalCreationOptions = {};
if (isResource(resource)) {
options.resource = uri;
} else {
options.interpreter = resource;
}
const executionInfo = await this.getExecutionInfo(name, resource, flags);
const terminalService = this.serviceContainer
.get<ITerminalServiceFactory>(ITerminalServiceFactory)
.getTerminalService(options);

const install = async (token?: CancellationToken) => {
const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource);
if (executionInfo.moduleName) {
Expand All @@ -64,25 +62,38 @@ export abstract class ModuleInstaller implements IModuleInstaller {
const interpreter = isResource(resource)
? await interpreterService.getActiveInterpreter(resource)
: resource;
const pythonPath = isResource(resource) ? settings.pythonPath : resource.path;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.pythonPath can also be path to environment, so use interpreter.path.

const interpreterPath = interpreter?.path ?? settings.pythonPath;
const pythonPath = isResource(resource) ? interpreterPath : resource.path;
const args = internalPython.execModule(executionInfo.moduleName, executionInfoArgs);
if (!interpreter || interpreter.envType !== EnvironmentType.Unknown) {
await terminalService.sendCommand(pythonPath, args, token);
await this.executeCommand(shouldExecuteInTerminal, resource, pythonPath, args, token);
} else if (settings.globalModuleInstallation) {
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
if (await fs.isDirReadonly(path.dirname(pythonPath)).catch((_err) => true)) {
this.elevatedInstall(pythonPath, args);
} else {
await terminalService.sendCommand(pythonPath, args, token);
await this.executeCommand(shouldExecuteInTerminal, resource, pythonPath, args, token);
}
} else if (name === translateProductToModule(Product.pip)) {
// Pip should always be installed into the specified environment.
await terminalService.sendCommand(pythonPath, args, token);
await this.executeCommand(shouldExecuteInTerminal, resource, pythonPath, args, token);
} else {
await terminalService.sendCommand(pythonPath, args.concat(['--user']), token);
await this.executeCommand(
shouldExecuteInTerminal,
resource,
pythonPath,
args.concat(['--user']),
token,
);
}
} else {
await terminalService.sendCommand(executionInfo.execPath!, executionInfoArgs, token);
await this.executeCommand(
shouldExecuteInTerminal,
resource,
executionInfo.execPath!,
executionInfoArgs,
token,
);
}
};

Expand All @@ -103,6 +114,7 @@ export abstract class ModuleInstaller implements IModuleInstaller {
await install(cancel);
}
}

public abstract isSupported(resource?: InterpreterUri): Promise<boolean>;

protected elevatedInstall(execPath: string, args: string[]) {
Expand Down Expand Up @@ -131,11 +143,13 @@ export abstract class ModuleInstaller implements IModuleInstaller {
}
});
}

protected abstract getExecutionInfo(
moduleName: string,
resource?: InterpreterUri,
flags?: ModuleInstallFlags,
): Promise<ExecutionInfo>;

private async processInstallArgs(args: string[], resource?: InterpreterUri): Promise<string[]> {
const indexOfPylint = args.findIndex((arg) => arg.toUpperCase() === 'PYLINT');
if (indexOfPylint === -1) {
Expand All @@ -152,6 +166,32 @@ export abstract class ModuleInstaller implements IModuleInstaller {
}
return args;
}

private async executeCommand(
executeInTerminal: boolean,
resource: InterpreterUri | undefined,
command: string,
args: string[],
token?: CancellationToken,
) {
const options: TerminalCreationOptions = {};
if (isResource(resource)) {
options.resource = resource;
} else {
options.interpreter = resource;
}
if (executeInTerminal) {
const terminalService = this.serviceContainer
.get<ITerminalServiceFactory>(ITerminalServiceFactory)
.getTerminalService(options);

terminalService.sendCommand(command, args, token);
} else {
const processServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
const processService = await processServiceFactory.create(options.resource);
await processService.exec(command, args);
}
}
}

export function translateProductToModule(product: Product): string {
Expand Down Expand Up @@ -204,6 +244,8 @@ export function translateProductToModule(product: Product): string {
return 'pip';
case Product.ensurepip:
return 'ensurepip';
case Product.python:
return 'python';
default: {
throw new Error(`Product ${product} cannot be installed as a Python Module.`);
}
Expand Down
27 changes: 25 additions & 2 deletions src/client/common/installer/pipInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { inject, injectable } from 'inversify';
import { IServiceContainer } from '../../ioc/types';
import { ModuleInstallerType } from '../../pythonEnvironments/info';
import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info';
import { IWorkspaceService } from '../application/types';
import { IPythonExecutionFactory } from '../process/types';
import { ExecutionInfo, IInstaller, Product } from '../types';
Expand All @@ -16,6 +16,26 @@ import { ProductNames } from './productNames';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import { IInterpreterService } from '../../interpreter/contracts';
import { isParentPath } from '../platform/fs-paths';

async function doesEnvironmentContainPython(serviceContainer: IServiceContainer, resource: InterpreterUri) {
const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
const environment = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource;
if (!environment) {
return undefined;
}
if (
environment.envPath?.length &&
environment.envType === EnvironmentType.Conda &&
!isParentPath(environment?.path, environment.envPath)
) {
// For conda environments not containing a python interpreter, do not use pip installer due to bugs in `conda run`:
// https://github.com/microsoft/vscode-python/issues/18479#issuecomment-1044427511
// https://github.com/conda/conda/issues/11211
Comment on lines +32 to +34
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, conda run -n <envname> python -m pip install <module> doesn't work.

return false;
}
return true;
}

@injectable()
export class PipInstaller extends ModuleInstaller {
Expand All @@ -36,7 +56,10 @@ export class PipInstaller extends ModuleInstaller {
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
super(serviceContainer);
}
public isSupported(resource?: InterpreterUri): Promise<boolean> {
public async isSupported(resource?: InterpreterUri): Promise<boolean> {
if ((await doesEnvironmentContainPython(this.serviceContainer, resource)) === false) {
return false;
}
return this.isPipAvailable(resource);
}
protected async getExecutionInfo(
Expand Down
Loading