Skip to content

Commit b543408

Browse files
Add support for "conda run" in PythonExecutionFactory (#8259)
* Initial commit for conda run * Forgot djangoShellCodeExecution.ts * Fix news file * Fix single workspace tests * Don't use interpreter in terminal code exec * Try increasing activation timeout time for smoke tests * Try different timeouts * try commenting conda in pythonExecutionFactory * Undo timeout changes * forgot await lol * Uncomment things * Small fixes + remove empty line * Reduce PR noise by removing some autoformatting * Update createActivatedEnv + minor refactoring * Add pythonExecutionFactory unit tests * Add pythonExecutionFactory unit tests * Fix conda env name check + django terminal * Unit tests for terminalCodeExecution.ts * Fix linting functional tests * djangoShellCodeExecution tests + fix terminalCodeExec.unit.test.ts * Make conda path check more explicit * Add Windows test to PythonExecutionFactory * Add conda call verification * Add CondaEnvironmentInfo type * CondaExecutionService unit tests * Add comment * Remove autformatting * Make getExecutableInfo public * Use CondaExecutionService in terminal code execution * Apply suggestions from code review Co-Authored-By: Eric Snow <[email protected]> * refactor CondaExecutionService instantiation * getExecutableInfo unit tests + more concise conda getExecutableInfo * createCondaExecutionService unit tests * Add conda version check * Housekeeping * Move terminal changes to another PR * Typo * Revert execObservable and execModuleObservable * Update GH link * More housekeeping * Fix compilation errors * Undo formatting changes * More formatting reverting * Try creating a conda environment first * Fix tests * Drop unnecessary command arg in getExecutionInfo * Fix PythonDaemon getExecutionInfo * Undo formatting changes * Fix merge gone sideways * Fix linting functional tests * Fix unit tests * Revert "Move terminal changes to another PR" This reverts commit 689bf1f. * Fixes * Fix tests * Fix single workspace tests * Remove unused declaration * Move createConda around in createActivatedEnv * Fix unit tests
1 parent eddbb65 commit b543408

32 files changed

+687
-103
lines changed

news/3 Code Health/7696.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Use "conda run" (instead of using the "python.pythonPath" setting directly) when executing
2+
Python and an Anaconda environment is selected.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import { injectable } from 'inversify';
4+
import { CondaEnvironmentInfo } from '../../interpreter/contracts';
5+
import { IServiceContainer } from '../../ioc/types';
6+
import { PythonExecutionService } from './pythonProcess';
7+
import { IProcessService, PythonExecutionInfo } from './types';
8+
9+
@injectable()
10+
export class CondaExecutionService extends PythonExecutionService {
11+
constructor(
12+
serviceContainer: IServiceContainer,
13+
procService: IProcessService,
14+
pythonPath: string,
15+
private readonly condaFile: string,
16+
private readonly condaEnvironment: CondaEnvironmentInfo
17+
) {
18+
super(serviceContainer, procService, pythonPath);
19+
}
20+
21+
public getExecutionInfo(args: string[]): PythonExecutionInfo {
22+
const executionArgs = this.condaEnvironment.name !== '' ? ['-n', this.condaEnvironment.name] : ['-p', this.condaEnvironment.path];
23+
24+
return { command: this.condaFile, args: ['run', ...executionArgs, 'python', ...args] };
25+
}
26+
}

src/client/common/process/pythonDaemon.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
IPythonExecutionService,
2222
ObservableExecutionResult,
2323
Output,
24+
PythonExecutionInfo,
2425
PythonVersionInfo,
2526
SpawnOptions,
2627
StdErrError
@@ -96,6 +97,9 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
9697
return this.pythonExecutionService.getExecutablePath();
9798
}
9899
}
100+
public getExecutionInfo(args: string[]): PythonExecutionInfo {
101+
return this.pythonExecutionService.getExecutionInfo(args);
102+
}
99103
public async isModuleInstalled(moduleName: string): Promise<boolean> {
100104
this.throwIfRPCConnectionIsDead();
101105
try {

src/client/common/process/pythonDaemonPool.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
IPythonDaemonExecutionService,
2323
IPythonExecutionService,
2424
ObservableExecutionResult,
25+
PythonExecutionInfo,
2526
SpawnOptions
2627
} from './types';
2728

@@ -71,6 +72,9 @@ export class PythonDaemonExecutionServicePool implements IPythonDaemonExecutionS
7172
this.logger.logProcess(`${this.pythonPath} (daemon)`, ['getExecutablePath']);
7273
return this.wrapCall(daemon => daemon.getExecutablePath());
7374
}
75+
public getExecutionInfo(args: string[]): PythonExecutionInfo {
76+
return this.pythonExecutionService.getExecutionInfo(args);
77+
}
7478
public async isModuleInstalled(moduleName: string): Promise<boolean> {
7579
this.logger.logProcess(`${this.pythonPath} (daemon)`, ['-m', moduleName]);
7680
return this.wrapCall(daemon => daemon.isModuleInstalled(moduleName));

src/client/common/process/pythonExecutionFactory.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
import { inject, injectable } from 'inversify';
4+
import { gte } from 'semver';
45

6+
import { Uri } from 'vscode';
57
import { IEnvironmentActivationService } from '../../interpreter/activation/types';
6-
import { IInterpreterService } from '../../interpreter/contracts';
8+
import { ICondaService, IInterpreterService } from '../../interpreter/contracts';
79
import { WindowsStoreInterpreter } from '../../interpreter/locators/services/windowsStoreInterpreter';
810
import { IWindowsStoreInterpreter } from '../../interpreter/locators/types';
911
import { IServiceContainer } from '../../ioc/types';
1012
import { sendTelemetryEvent } from '../../telemetry';
1113
import { EventName } from '../../telemetry/constants';
1214
import { traceError } from '../logger';
1315
import { IConfigurationService, IDisposableRegistry } from '../types';
16+
import { CondaExecutionService } from './condaExecutionService';
1417
import { ProcessService } from './proc';
1518
import { PythonDaemonExecutionServicePool } from './pythonDaemonPool';
1619
import { PythonExecutionService } from './pythonProcess';
@@ -28,6 +31,9 @@ import {
2831
} from './types';
2932
import { WindowsStorePythonProcess } from './windowsStorePythonProcess';
3033

34+
// Minimum version number of conda required to be able to use 'conda run'
35+
export const CONDA_RUN_VERSION = '4.6.0';
36+
3137
@injectable()
3238
export class PythonExecutionFactory implements IPythonExecutionFactory {
3339
private readonly daemonsPerPythonService = new Map<string, Promise<IPythonDaemonExecutionService>>();
@@ -36,6 +42,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
3642
@inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService,
3743
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory,
3844
@inject(IConfigurationService) private readonly configService: IConfigurationService,
45+
@inject(ICondaService) private readonly condaService: ICondaService,
3946
@inject(IBufferDecoder) private readonly decoder: IBufferDecoder,
4047
@inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter
4148
) {}
@@ -44,6 +51,18 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
4451
const processService: IProcessService = await this.processServiceFactory.create(options.resource);
4552
const processLogger = this.serviceContainer.get<IProcessLogger>(IProcessLogger);
4653
processService.on('exec', processLogger.logProcess.bind(processLogger));
54+
55+
// Don't bother getting a conda execution service instance if we haven't fetched the list of interpreters yet.
56+
// Also, without this hasInterpreters check smoke tests will time out
57+
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
58+
const hasInterpreters = await interpreterService.hasInterpreters;
59+
if (hasInterpreters) {
60+
const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService);
61+
if (condaExecutionService) {
62+
return condaExecutionService;
63+
}
64+
}
65+
4766
if (this.windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)) {
4867
return new WindowsStorePythonProcess(this.serviceContainer, processService, pythonPath, this.windowsStoreInterpreter);
4968
}
@@ -98,6 +117,33 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
98117
const processLogger = this.serviceContainer.get<IProcessLogger>(IProcessLogger);
99118
processService.on('exec', processLogger.logProcess.bind(processLogger));
100119
this.serviceContainer.get<IDisposableRegistry>(IDisposableRegistry).push(processService);
120+
121+
const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService);
122+
if (condaExecutionService) {
123+
return condaExecutionService;
124+
}
125+
101126
return new PythonExecutionService(this.serviceContainer, processService, pythonPath);
102127
}
128+
public async createCondaExecutionService(pythonPath: string, processService?: IProcessService, resource?: Uri): Promise<CondaExecutionService | undefined> {
129+
const processServicePromise = processService ? Promise.resolve(processService) : this.processServiceFactory.create(resource);
130+
const [condaVersion, condaEnvironment, condaFile, procService] = await Promise.all([
131+
this.condaService.getCondaVersion(),
132+
this.condaService.getCondaEnvironment(pythonPath),
133+
this.condaService.getCondaFile(),
134+
processServicePromise
135+
]);
136+
137+
if (condaVersion && gte(condaVersion, CONDA_RUN_VERSION) && condaEnvironment && condaFile && procService) {
138+
// Add logging to the newly created process service
139+
if (!processService) {
140+
const processLogger = this.serviceContainer.get<IProcessLogger>(IProcessLogger);
141+
procService.on('exec', processLogger.logProcess.bind(processLogger));
142+
this.serviceContainer.get<IDisposableRegistry>(IDisposableRegistry).push(procService);
143+
}
144+
return new CondaExecutionService(this.serviceContainer, procService, pythonPath, condaFile, condaEnvironment);
145+
}
146+
147+
return Promise.resolve(undefined);
148+
}
103149
}

src/client/common/process/pythonProcess.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
IProcessService,
1919
IPythonExecutionService,
2020
ObservableExecutionResult,
21+
PythonExecutionInfo,
2122
PythonVersionInfo,
2223
SpawnOptions
2324
} from './types';
@@ -41,7 +42,8 @@ export class PythonExecutionService implements IPythonExecutionService {
4142
// See these two bugs:
4243
// https://github.com/microsoft/vscode-python/issues/7569
4344
// https://github.com/microsoft/vscode-python/issues/7760
44-
const jsonValue = await waitForPromise(this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true }), 5000)
45+
const { command, args } = this.getExecutionInfo([file]);
46+
const jsonValue = await waitForPromise(this.procService.exec(command, args, { mergeStdOutErr: true }), 5000)
4547
.then(output => output ? output.stdout.trim() : '--timed out--'); // --timed out-- should cause an exception
4648

4749
let json: { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean };
@@ -69,29 +71,41 @@ export class PythonExecutionService implements IPythonExecutionService {
6971
if (await this.fileSystem.fileExists(this.pythonPath)) {
7072
return this.pythonPath;
7173
}
72-
return this.procService.exec(this.pythonPath, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true })
73-
.then(output => output.stdout.trim());
74+
75+
const { command, args } = this.getExecutionInfo(['-c', 'import sys;print(sys.executable)']);
76+
return this.procService.exec(command, args, { throwOnStdErr: true }).then(output => output.stdout.trim());
7477
}
7578
public async isModuleInstalled(moduleName: string): Promise<boolean> {
76-
return this.procService.exec(this.pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })
79+
const { command, args } = this.getExecutionInfo(['-c', `import ${moduleName}`]);
80+
return this.procService.exec(command, args, { throwOnStdErr: true })
7781
.then(() => true).catch(() => false);
7882
}
7983

84+
public getExecutionInfo(args: string[]): PythonExecutionInfo {
85+
return { command: this.pythonPath, args };
86+
}
87+
8088
public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
8189
const opts: SpawnOptions = { ...options };
90+
// Cannot use this.getExecutionInfo() until 'conda run' can be run without buffering output.
91+
// See https://github.com/microsoft/vscode-python/issues/8473
8292
return this.procService.execObservable(this.pythonPath, args, opts);
8393
}
8494
public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
8595
const opts: SpawnOptions = { ...options };
96+
// Cannot use this.getExecutionInfo() until 'conda run' can be run without buffering output.
97+
// See https://github.com/microsoft/vscode-python/issues/8473
8698
return this.procService.execObservable(this.pythonPath, ['-m', moduleName, ...args], opts);
8799
}
88100
public async exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> {
89101
const opts: SpawnOptions = { ...options };
90-
return this.procService.exec(this.pythonPath, args, opts);
102+
const executable = this.getExecutionInfo(args);
103+
return this.procService.exec(executable.command, executable.args, opts);
91104
}
92105
public async execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> {
93106
const opts: SpawnOptions = { ...options };
94-
const result = await this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts);
107+
const executable = this.getExecutionInfo(['-m', moduleName, ...args]);
108+
const result = await this.procService.exec(executable.command, executable.args, opts);
95109

96110
// If a module is not installed we'll have something in stderr.
97111
if (moduleName && ErrorUtils.outputHasModuleNotInstalledError(moduleName!, result.stderr)) {

src/client/common/process/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Newable } from '../../ioc/types';
99
import { ExecutionInfo, IDisposable, Version } from '../types';
1010
import { Architecture } from '../utils/platform';
1111
import { EnvironmentVariables } from '../variables/types';
12+
import { CondaExecutionService } from './condaExecutionService';
1213

1314
export const IBufferDecoder = Symbol('IBufferDecoder');
1415
export interface IBufferDecoder {
@@ -115,6 +116,7 @@ export interface IPythonExecutionFactory {
115116
*/
116117
createDaemon(options: DaemonExecutionFactoryCreationOptions): Promise<IPythonExecutionService>;
117118
createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise<IPythonExecutionService>;
119+
createCondaExecutionService(pythonPath: string, processService?: IProcessService, resource?: Uri): Promise<CondaExecutionService | undefined>;
118120
}
119121
export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown';
120122
export type PythonVersionInfo = [number, number, number, ReleaseLevel];
@@ -132,6 +134,7 @@ export interface IPythonExecutionService {
132134
getInterpreterInformation(): Promise<InterpreterInfomation | undefined>;
133135
getExecutablePath(): Promise<string>;
134136
isModuleInstalled(moduleName: string): Promise<boolean>;
137+
getExecutionInfo(args: string[]): PythonExecutionInfo;
135138

136139
execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string>;
137140
execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string>;
@@ -140,6 +143,10 @@ export interface IPythonExecutionService {
140143
execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>;
141144
}
142145

146+
export type PythonExecutionInfo = {
147+
command: string;
148+
args: string[];
149+
};
143150
/**
144151
* Identical to the PythonExecutionService, but with a `dispose` method.
145152
* This is a daemon process that lives on until it is disposed, hence the `IDisposable`.

src/client/interpreter/contracts.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ export type CondaInfo = {
4343
conda_version?: string;
4444
};
4545

46+
export type CondaEnvironmentInfo = {
47+
name: string;
48+
path: string;
49+
};
50+
4651
export const ICondaService = Symbol('ICondaService');
4752

4853
export interface ICondaService {
@@ -51,11 +56,11 @@ export interface ICondaService {
5156
isCondaAvailable(): Promise<boolean>;
5257
getCondaVersion(): Promise<SemVer | undefined>;
5358
getCondaInfo(): Promise<CondaInfo | undefined>;
54-
getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>;
59+
getCondaEnvironments(ignoreCache: boolean): Promise<CondaEnvironmentInfo[] | undefined>;
5560
getInterpreterPath(condaEnvironmentPath: string): string;
5661
getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise<string | undefined>;
5762
isCondaEnvironment(interpreterPath: string): Promise<boolean>;
58-
getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined>;
63+
getCondaEnvironment(interpreterPath: string): Promise<CondaEnvironmentInfo | undefined>;
5964
}
6065

6166
export enum InterpreterType {

src/client/interpreter/locators/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi
5151
return new EventEmitter<Promise<PythonInterpreter[]>>().event;
5252
}
5353
public get hasInterpreters(): Promise<boolean> {
54-
return this._hasInterpreters.promise;
54+
return this._hasInterpreters.completed ? this._hasInterpreters.promise : Promise.resolve(false);
5555
}
5656

5757
/**

src/client/interpreter/locators/services/cacheableLocatorService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
3434
return this.locating.event;
3535
}
3636
public get hasInterpreters(): Promise<boolean> {
37-
return this._hasInterpreters.promise;
37+
return this._hasInterpreters.completed ? this._hasInterpreters.promise : Promise.resolve(false);
3838
}
3939
public abstract dispose(): void;
4040
@traceDecorators.verbose('Get Interpreters in CacheableLocatorService')

src/client/interpreter/locators/services/condaService.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IProcessServiceFactory } from '../../../common/process/types';
1010
import { IConfigurationService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../../../common/types';
1111
import { cache } from '../../../common/utils/decorators';
1212
import {
13+
CondaEnvironmentInfo,
1314
CondaInfo,
1415
ICondaService,
1516
IInterpreterLocatorService,
@@ -219,10 +220,10 @@ export class CondaService implements ICondaService {
219220
* Return the list of conda envs (by name, interpreter filename).
220221
*/
221222
@traceDecorators.verbose('Get Conda environments')
222-
public async getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined> {
223+
public async getCondaEnvironments(ignoreCache: boolean): Promise<CondaEnvironmentInfo[] | undefined> {
223224
// Global cache.
224225
// tslint:disable-next-line:no-any
225-
const globalPersistence = this.persistentStateFactory.createGlobalPersistentState<{ data: { name: string; path: string }[] | undefined }>('CONDA_ENVIRONMENTS', undefined as any);
226+
const globalPersistence = this.persistentStateFactory.createGlobalPersistentState<{ data: CondaEnvironmentInfo[] | undefined }>('CONDA_ENVIRONMENTS', undefined as any);
226227
if (!ignoreCache && globalPersistence.value) {
227228
return globalPersistence.value.data;
228229
}

0 commit comments

Comments
 (0)