Skip to content

Commit f1d0509

Browse files
author
Kartik Raj
authored
Add support for detection and selection of conda environments lacking a python interpreter (#18427)
* Support conda environments without a python executable * Fix interpreter display * Fix areSameEnv * Add support to select env folders as interpreterPaths * Allow selecting such environments * Handle resolving environment path * Document resolver works for both env path and executable * Fxi bug * Simplify conda.ts * Define identifiers for an environment * Fix getCondaEnvironment * Introduce an id property to environments * Update proposed discovery API to handle environments * Fix bug with interpreter display * Normalize path passed in resolveEnv * Update environment details API * Dont use pythonPath for getting active interpreter for activation commands * Support conda activation * Support ${command:python.interpreterPath} with this envs * Fix getActiveItem * Add comment justifying using `.pythonPath` for middleware * Fix shebang codelens * Fix startup telemetry * Do not support pip installer for such environments * Trigger discovery once installation is finished for such environments * Automatically install python into environment once it is selected * Add telemetry for new interpreters discovered * Add telemtry if such an env is selected * Fix bugs and cache installer * Fix compile errors, tests, and add tests * Fix some tests * Ignore id when comparing envs * Phew, fixed terrible tests in module installer * Fix more tests * MOre * Skip resolver tests on linux * Fix tests * If environment doesn't contain python do not support pip installer * Skip module install tests for python as it is not a module * News entry * Remove unnecssary commnet * Fix bug introduced by merges * Code reviews
1 parent ec54e1a commit f1d0509

File tree

72 files changed

+1146
-594
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1146
-594
lines changed

news/1 Enhancements/18357.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for detection and selection of conda environments lacking a python interpreter.

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"Pylance.pylanceRevertToJedi": "Revert to Jedi",
4646
"Experiments.inGroup": "Experiment '{0}' is active",
4747
"Experiments.optedOutOf": "Experiment '{0}' is inactive",
48+
"Interpreters.installingPython": "Installing Python into Environment...",
4849
"Interpreters.clearAtWorkspace": "Clear at workspace level",
4950
"Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters",
5051
"Interpreters.entireWorkspace": "Select at workspace level",

src/client/activation/languageClientMiddlewareBase.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export class LanguageClientMiddlewareBase implements Middleware {
8080
const uri = item.scopeUri ? Uri.parse(item.scopeUri) : undefined;
8181
// For backwards compatibility, set python.pythonPath to the configured
8282
// value as though it were in the user's settings.json file.
83+
// As this is for backwards compatibility, `ConfigService.pythonPath`
84+
// can be considered as active interpreter path.
8385
settings[i].pythonPath = configService.getSettings(uri).pythonPath;
8486

8587
const env = await envService.getEnvironmentVariables(uri);

src/client/apiTypes.ts

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { Event, Uri } from 'vscode';
55
import { Resource } from './common/types';
66
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
7+
import { EnvPathType, PythonEnvKind } from './pythonEnvironments/base/info';
78

89
/*
910
* Do not introduce any breaking changes to this API.
@@ -87,28 +88,39 @@ export interface IExtensionApi {
8788
};
8889
}
8990

90-
export interface InterpreterDetailsOptions {
91+
export interface EnvironmentDetailsOptions {
9192
useCache: boolean;
9293
}
9394

94-
export interface InterpreterDetails {
95-
path: string;
95+
export interface EnvironmentDetails {
96+
interpreterPath: string;
97+
envFolderPath?: string;
9698
version: string[];
97-
environmentType: string[];
99+
environmentType: PythonEnvKind[];
98100
metadata: Record<string, unknown>;
99101
}
100102

101-
export interface InterpretersChangedParams {
103+
export interface EnvironmentsChangedParams {
104+
/**
105+
* Path to environment folder or path to interpreter that uniquely identifies an environment.
106+
* Virtual environments lacking an interpreter are identified by environment folder paths,
107+
* whereas other envs can be identified using interpreter path.
108+
*/
102109
path?: string;
103110
type: 'add' | 'remove' | 'update' | 'clear-all';
104111
}
105112

106-
export interface ActiveInterpreterChangedParams {
107-
interpreterPath?: string;
113+
export interface ActiveEnvironmentChangedParams {
114+
/**
115+
* Path to environment folder or path to interpreter that uniquely identifies an environment.
116+
* Virtual environments lacking an interpreter are identified by environment folder paths,
117+
* whereas other envs can be identified using interpreter path.
118+
*/
119+
path: string;
108120
resource?: Uri;
109121
}
110122

111-
export interface RefreshInterpretersOptions {
123+
export interface RefreshEnvironmentsOptions {
112124
clearCache?: boolean;
113125
}
114126

@@ -122,57 +134,60 @@ export interface IProposedExtensionAPI {
122134
* returns what ever is set for the workspace.
123135
* @param resource : Uri of a file or workspace
124136
*/
125-
getActiveInterpreterPath(resource?: Resource): Promise<string | undefined>;
137+
getActiveEnvironmentPath(resource?: Resource): Promise<EnvPathType | undefined>;
126138
/**
127139
* Returns details for the given interpreter. Details such as absolute interpreter path,
128140
* version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under
129141
* metadata field.
130-
* @param interpreterPath : Path of the interpreter whose details you need.
142+
* @param path : Path to environment folder or path to interpreter whose details you need.
131143
* @param options : [optional]
132144
* * useCache : When true, cache is checked first for any data, returns even if there
133145
* is partial data.
134146
*/
135-
getInterpreterDetails(
136-
interpreterPath: string,
137-
options?: InterpreterDetailsOptions,
138-
): Promise<InterpreterDetails | undefined>;
147+
getEnvironmentDetails(
148+
path: string,
149+
options?: EnvironmentDetailsOptions,
150+
): Promise<EnvironmentDetails | undefined>;
139151
/**
140-
* Returns paths to interpreters found by the extension at the time of calling. This API
141-
* will *not* trigger a refresh. If a refresh is going on it will *not* wait for the refresh
142-
* to finish. This will return what is known so far. To get complete list `await` on promise
143-
* returned by `getRefreshPromise()`.
152+
* Returns paths to environments that uniquely identifies an environment found by the extension
153+
* at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it
154+
* will *not* wait for the refresh to finish. This will return what is known so far. To get
155+
* complete list `await` on promise returned by `getRefreshPromise()`.
156+
*
157+
* Virtual environments lacking an interpreter are identified by environment folder paths,
158+
* whereas other envs can be identified using interpreter path.
144159
*/
145-
getInterpreterPaths(): Promise<string[] | undefined>;
160+
getEnvironmentPaths(): Promise<EnvPathType[] | undefined>;
146161
/**
147-
* Sets the active interpreter path for the python extension. Configuration target will
148-
* always be the workspace.
149-
* @param interpreterPath : Interpreter path to set for a given workspace.
162+
* Sets the active environment path for the python extension. Configuration target will
163+
* always be the workspace folder.
164+
* @param path : Interpreter path to set for a given workspace.
150165
* @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace
151166
* folder.
152167
*/
153-
setActiveInterpreter(interpreterPath: string, resource?: Resource): Promise<void>;
168+
setActiveEnvironment(path: string, resource?: Resource): Promise<void>;
154169
/**
155170
* This API will re-trigger environment discovery. Extensions can wait on the returned
156-
* promise to get the updated interpreters list. If there is a refresh already going on
171+
* promise to get the updated environment list. If there is a refresh already going on
157172
* then it returns the promise for that refresh.
158173
* @param options : [optional]
159174
* * clearCache : When true, this will clear the cache before interpreter refresh
160175
* is triggered.
161176
*/
162-
refreshInterpreters(options?: RefreshInterpretersOptions): Promise<string[] | undefined>;
177+
refreshEnvironment(options?: RefreshEnvironmentsOptions): Promise<EnvPathType[] | undefined>;
163178
/**
164179
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
165180
* refreshes going on.
166181
*/
167182
getRefreshPromise(): Promise<void> | undefined;
168183
/**
169-
* This event is triggered when the known interpreters list changes, like when a interpreter
170-
* is found, existing interpreter is removed, or some details changed on an interpreter.
184+
* This event is triggered when the known environment list changes, like when a environment
185+
* is found, existing environment is removed, or some details changed on an environment.
171186
*/
172-
onDidInterpretersChanged: Event<InterpretersChangedParams[]>;
187+
onDidEnvironmentsChanged: Event<EnvironmentsChangedParams[]>;
173188
/**
174-
* This event is triggered when the active interpreter changes.
189+
* This event is triggered when the active environment changes.
175190
*/
176-
onDidActiveInterpreterChanged: Event<ActiveInterpreterChangedParams>;
191+
onDidActiveEnvironmentChanged: Event<ActiveEnvironmentChangedParams>;
177192
};
178193
}

src/client/common/installer/condaInstaller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class CondaInstaller extends ModuleInstaller {
7575

7676
const pythonPath = isResource(resource)
7777
? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath
78-
: resource.path;
78+
: resource.id ?? '';
7979
const condaLocatorService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter);
8080
const info = await condaLocatorService.getCondaEnvironment(pythonPath);
8181
const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install'];
@@ -127,7 +127,7 @@ export class CondaInstaller extends ModuleInstaller {
127127
const condaService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter);
128128
const pythonPath = isResource(resource)
129129
? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath
130-
: resource.path;
130+
: resource.id ?? '';
131131
return condaService.isCondaEnvironment(pythonPath);
132132
}
133133
}

src/client/common/installer/moduleInstaller.ts

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@ import { wrapCancellationTokens } from '../cancellation';
1515
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
1616
import { IFileSystem } from '../platform/types';
1717
import * as internalPython from '../process/internal/python';
18+
import { IProcessServiceFactory } from '../process/types';
1819
import { ITerminalServiceFactory, TerminalCreationOptions } from '../terminal/types';
1920
import { ExecutionInfo, IConfigurationService, IOutputChannel, Product } from '../types';
2021
import { Products } from '../utils/localize';
2122
import { isResource } from '../utils/misc';
2223
import { ProductNames } from './productNames';
23-
import { IModuleInstaller, InterpreterUri, ModuleInstallFlags } from './types';
24+
import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types';
2425

2526
@injectable()
2627
export abstract class ModuleInstaller implements IModuleInstaller {
2728
public abstract get priority(): number;
29+
2830
public abstract get name(): string;
31+
2932
public abstract get displayName(): string;
33+
3034
public abstract get type(): ModuleInstallerType;
3135

3236
constructor(protected serviceContainer: IServiceContainer) {}
@@ -36,24 +40,18 @@ export abstract class ModuleInstaller implements IModuleInstaller {
3640
resource?: InterpreterUri,
3741
cancel?: CancellationToken,
3842
flags?: ModuleInstallFlags,
43+
options?: InstallOptions,
3944
): Promise<void> {
45+
const shouldExecuteInTerminal = !options?.installAsProcess;
4046
const name =
41-
typeof productOrModuleName == 'string'
47+
typeof productOrModuleName === 'string'
4248
? productOrModuleName
4349
: translateProductToModule(productOrModuleName);
4450
const productName = typeof productOrModuleName === 'string' ? name : ProductNames.get(productOrModuleName);
4551
sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { installer: this.displayName, productName });
4652
const uri = isResource(resource) ? resource : undefined;
47-
const options: TerminalCreationOptions = {};
48-
if (isResource(resource)) {
49-
options.resource = uri;
50-
} else {
51-
options.interpreter = resource;
52-
}
5353
const executionInfo = await this.getExecutionInfo(name, resource, flags);
54-
const terminalService = this.serviceContainer
55-
.get<ITerminalServiceFactory>(ITerminalServiceFactory)
56-
.getTerminalService(options);
54+
5755
const install = async (token?: CancellationToken) => {
5856
const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource);
5957
if (executionInfo.moduleName) {
@@ -64,25 +62,38 @@ export abstract class ModuleInstaller implements IModuleInstaller {
6462
const interpreter = isResource(resource)
6563
? await interpreterService.getActiveInterpreter(resource)
6664
: resource;
67-
const pythonPath = isResource(resource) ? settings.pythonPath : resource.path;
65+
const interpreterPath = interpreter?.path ?? settings.pythonPath;
66+
const pythonPath = isResource(resource) ? interpreterPath : resource.path;
6867
const args = internalPython.execModule(executionInfo.moduleName, executionInfoArgs);
6968
if (!interpreter || interpreter.envType !== EnvironmentType.Unknown) {
70-
await terminalService.sendCommand(pythonPath, args, token);
69+
await this.executeCommand(shouldExecuteInTerminal, resource, pythonPath, args, token);
7170
} else if (settings.globalModuleInstallation) {
7271
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
7372
if (await fs.isDirReadonly(path.dirname(pythonPath)).catch((_err) => true)) {
7473
this.elevatedInstall(pythonPath, args);
7574
} else {
76-
await terminalService.sendCommand(pythonPath, args, token);
75+
await this.executeCommand(shouldExecuteInTerminal, resource, pythonPath, args, token);
7776
}
7877
} else if (name === translateProductToModule(Product.pip)) {
7978
// Pip should always be installed into the specified environment.
80-
await terminalService.sendCommand(pythonPath, args, token);
79+
await this.executeCommand(shouldExecuteInTerminal, resource, pythonPath, args, token);
8180
} else {
82-
await terminalService.sendCommand(pythonPath, args.concat(['--user']), token);
81+
await this.executeCommand(
82+
shouldExecuteInTerminal,
83+
resource,
84+
pythonPath,
85+
args.concat(['--user']),
86+
token,
87+
);
8388
}
8489
} else {
85-
await terminalService.sendCommand(executionInfo.execPath!, executionInfoArgs, token);
90+
await this.executeCommand(
91+
shouldExecuteInTerminal,
92+
resource,
93+
executionInfo.execPath!,
94+
executionInfoArgs,
95+
token,
96+
);
8697
}
8798
};
8899

@@ -103,6 +114,7 @@ export abstract class ModuleInstaller implements IModuleInstaller {
103114
await install(cancel);
104115
}
105116
}
117+
106118
public abstract isSupported(resource?: InterpreterUri): Promise<boolean>;
107119

108120
protected elevatedInstall(execPath: string, args: string[]) {
@@ -131,11 +143,13 @@ export abstract class ModuleInstaller implements IModuleInstaller {
131143
}
132144
});
133145
}
146+
134147
protected abstract getExecutionInfo(
135148
moduleName: string,
136149
resource?: InterpreterUri,
137150
flags?: ModuleInstallFlags,
138151
): Promise<ExecutionInfo>;
152+
139153
private async processInstallArgs(args: string[], resource?: InterpreterUri): Promise<string[]> {
140154
const indexOfPylint = args.findIndex((arg) => arg.toUpperCase() === 'PYLINT');
141155
if (indexOfPylint === -1) {
@@ -152,6 +166,32 @@ export abstract class ModuleInstaller implements IModuleInstaller {
152166
}
153167
return args;
154168
}
169+
170+
private async executeCommand(
171+
executeInTerminal: boolean,
172+
resource: InterpreterUri | undefined,
173+
command: string,
174+
args: string[],
175+
token?: CancellationToken,
176+
) {
177+
const options: TerminalCreationOptions = {};
178+
if (isResource(resource)) {
179+
options.resource = resource;
180+
} else {
181+
options.interpreter = resource;
182+
}
183+
if (executeInTerminal) {
184+
const terminalService = this.serviceContainer
185+
.get<ITerminalServiceFactory>(ITerminalServiceFactory)
186+
.getTerminalService(options);
187+
188+
terminalService.sendCommand(command, args, token);
189+
} else {
190+
const processServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
191+
const processService = await processServiceFactory.create(options.resource);
192+
await processService.exec(command, args);
193+
}
194+
}
155195
}
156196

157197
export function translateProductToModule(product: Product): string {
@@ -204,6 +244,8 @@ export function translateProductToModule(product: Product): string {
204244
return 'pip';
205245
case Product.ensurepip:
206246
return 'ensurepip';
247+
case Product.python:
248+
return 'python';
207249
default: {
208250
throw new Error(`Product ${product} cannot be installed as a Python Module.`);
209251
}

src/client/common/installer/pipInstaller.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { inject, injectable } from 'inversify';
55
import { IServiceContainer } from '../../ioc/types';
6-
import { ModuleInstallerType } from '../../pythonEnvironments/info';
6+
import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info';
77
import { IWorkspaceService } from '../application/types';
88
import { IPythonExecutionFactory } from '../process/types';
99
import { ExecutionInfo, IInstaller, Product } from '../types';
@@ -16,6 +16,26 @@ import { ProductNames } from './productNames';
1616
import { sendTelemetryEvent } from '../../telemetry';
1717
import { EventName } from '../../telemetry/constants';
1818
import { IInterpreterService } from '../../interpreter/contracts';
19+
import { isParentPath } from '../platform/fs-paths';
20+
21+
async function doesEnvironmentContainPython(serviceContainer: IServiceContainer, resource: InterpreterUri) {
22+
const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
23+
const environment = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource;
24+
if (!environment) {
25+
return undefined;
26+
}
27+
if (
28+
environment.envPath?.length &&
29+
environment.envType === EnvironmentType.Conda &&
30+
!isParentPath(environment?.path, environment.envPath)
31+
) {
32+
// For conda environments not containing a python interpreter, do not use pip installer due to bugs in `conda run`:
33+
// https://github.com/microsoft/vscode-python/issues/18479#issuecomment-1044427511
34+
// https://github.com/conda/conda/issues/11211
35+
return false;
36+
}
37+
return true;
38+
}
1939

2040
@injectable()
2141
export class PipInstaller extends ModuleInstaller {
@@ -36,7 +56,10 @@ export class PipInstaller extends ModuleInstaller {
3656
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
3757
super(serviceContainer);
3858
}
39-
public isSupported(resource?: InterpreterUri): Promise<boolean> {
59+
public async isSupported(resource?: InterpreterUri): Promise<boolean> {
60+
if ((await doesEnvironmentContainPython(this.serviceContainer, resource)) === false) {
61+
return false;
62+
}
4063
return this.isPipAvailable(resource);
4164
}
4265
protected async getExecutionInfo(

0 commit comments

Comments
 (0)