Skip to content

Commit 90f4cf2

Browse files
committed
feat: adds ability to detect Python virtual envs
1 parent 3daed0f commit 90f4cf2

File tree

14 files changed

+352
-136
lines changed

14 files changed

+352
-136
lines changed

package-lock.json

Lines changed: 109 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@
761761
"@typescript-eslint/parser": "^8.24.0",
762762
"@vscode/test-electron": "^2.4.1",
763763
"c8": "^10.1.3",
764+
"copy-webpack-plugin": "^12.0.2",
764765
"eslint": "^9.20.1",
765766
"eslint-plugin-import": "^2.29.1",
766767
"eslint-plugin-jsdoc": "^50.2.2",

scripts/get_pip_bin_dir.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import os
2+
import sysconfig
3+
4+
5+
def get_install_bin_dir() -> str:
6+
"""Get the path to the pip install directory for scripts (bin, Scripts).
7+
Works for virtualenv, conda, and system installs.
8+
9+
Returns
10+
-------
11+
str
12+
Path to the pip install directory for scripts (bin, Scripts).
13+
"""
14+
if ("VIRTUAL_ENV" in os.environ) or ("CONDA_PREFIX" in os.environ):
15+
return sysconfig.get_path("scripts")
16+
else:
17+
return sysconfig.get_path("scripts", f"{os.name}_user")
18+
19+
20+
if __name__ == "__main__":
21+
print(get_install_bin_dir())

src/format/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import * as vscode from 'vscode';
66
import which from 'which';
77

88
import { Logger } from '../services/logging';
9+
import { spawnAsPromise } from '../util/shell';
910
import {
1011
FORMATTERS,
1112
EXTENSION_ID,
1213
promptForMissingTool,
1314
getWholeFileRange,
14-
spawnAsPromise,
1515
pathRelToAbs,
1616
} from '../util/tools';
1717

src/lint/provider.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ import {
2121
import { Logger } from '../services/logging';
2222
import { GlobPaths } from '../util/glob-paths';
2323
import { arraysEqual } from '../util/helper';
24+
import { spawnAsPromise, shellTask } from '../util/shell';
2425
import {
2526
EXTENSION_ID,
2627
resolveVariables,
2728
promptForMissingTool,
2829
isFreeForm,
29-
spawnAsPromise,
3030
isFortran,
31-
shellTask,
3231
} from '../util/tools';
3332

3433
import { GNULinter, GNUModernLinter, IntelLinter, LFortranLinter, NAGLinter } from './compilers';

src/lsp/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-lan
1010

1111
import { RestartLS } from '../commands/commands';
1212
import { Logger } from '../services/logging';
13+
import { pipInstall } from '../util/python';
1314
import {
1415
EXTENSION_ID,
1516
FortranDocumentSelector,
1617
LS_NAME,
1718
isFortran,
1819
getOuterMostWorkspaceFolder,
19-
pipInstall,
2020
resolveVariables,
2121
} from '../util/tools';
2222

src/util/ms-python-api/jupyter/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ interface IJupyterServerUri {
99
baseUrl: string;
1010
token: string;
1111

12-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1312
authorizationHeader: any; // JSON object for authorization header.
1413
expiration?: Date; // Date/time when header expires and should be refreshed.
1514
displayName: string;
@@ -46,5 +45,4 @@ enum ColumnType {
4645
Bool = 'bool',
4746
}
4847

49-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5048
type IRowsResponse = any[];

src/util/python.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
import * as path from 'path';
4+
5+
import { extensions, Uri } from 'vscode';
6+
7+
import { IExtensionApi, ResolvedEnvironment } from './ms-python-api/types';
8+
import { shellTask, spawnAsPromise } from './shell';
9+
10+
/**
11+
* Get Python path from the workspace or the system
12+
*
13+
* @param resource file, folder or workspace to search for python
14+
* @returns string with path to python
15+
*/
16+
export async function getPythonPath(resource?: Uri): Promise<string> {
17+
const pythonEnv = await getPythonEnvMS(resource);
18+
if (pythonEnv) {
19+
return pythonEnv.path;
20+
}
21+
return process.platform === 'win32' ? 'python' : 'python3';
22+
}
23+
24+
/**
25+
* Get path that pip installs binaries into.
26+
* Useful, for when the path is not in the PATH environment variable.
27+
*
28+
* @param resource file, folder or workspace
29+
* @returns string with path to pip
30+
*/
31+
export async function getPipBinDir(resource?: Uri): Promise<string> {
32+
const py = await getPythonPath(resource);
33+
const script = path.join(__dirname, 'scripts', 'get_pip_bin_dir.py');
34+
const [stdout, stderr] = await spawnAsPromise(py, [script]);
35+
if (stderr) {
36+
throw new Error(stderr);
37+
}
38+
return stdout.trim();
39+
}
40+
41+
/**
42+
* A wrapper around a call to `pip` for installing external tools.
43+
* Does not explicitly check if `pip` is installed.
44+
*
45+
* @param pyPackage name of python package in PyPi
46+
*/
47+
export async function pipInstall(pyPackage: string): Promise<string> {
48+
const py = await getPythonPath();
49+
const args = ['-m', 'pip', 'install', '--user', '--upgrade', pyPackage];
50+
return await shellTask(py, args, `pip: ${pyPackage}`);
51+
}
52+
53+
/**
54+
* Get the active Python environment, if any, via the ms-python.python
55+
* extension API.
56+
*
57+
* @param resource file/folder/workspace Uri or undefined
58+
* @returns Path to the active Python environment or undefined
59+
*/
60+
export async function getPythonEnvMS(
61+
resource?: Uri | undefined
62+
): Promise<ResolvedEnvironment | undefined> {
63+
try {
64+
const extension = extensions.getExtension('ms-python.python');
65+
if (!extension) {
66+
return undefined; // extension not installed
67+
}
68+
if (!extension.isActive) {
69+
await extension.activate();
70+
}
71+
const pythonApi: IExtensionApi = extension.exports as IExtensionApi;
72+
const activeEnv: ResolvedEnvironment = await pythonApi.environments.resolveEnvironment(
73+
pythonApi.environments.getActiveEnvironmentPath(resource)
74+
);
75+
if (!activeEnv) {
76+
return undefined; // no active environment, unlikely but possible
77+
}
78+
return activeEnv;
79+
} catch (error) {
80+
return undefined;
81+
}
82+
}

src/util/shell.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
import * as cp from 'child_process';
4+
5+
import * as vscode from 'vscode';
6+
7+
export async function shellTask(command: string, args: string[], name: string): Promise<string> {
8+
const task = new vscode.Task(
9+
{ type: 'shell' },
10+
vscode.TaskScope.Workspace,
11+
name,
12+
'Modern Fortran',
13+
new vscode.ShellExecution(command, args)
14+
);
15+
// Temporay fix to https://github.com/microsoft/vscode/issues/157756
16+
(<vscode.Task>task).definition = { type: 'shell', command: command };
17+
const execution = await vscode.tasks.executeTask(task);
18+
return await new Promise<string>((resolve, reject) => {
19+
const disposable = vscode.tasks.onDidEndTaskProcess(e => {
20+
if (e.execution === execution) {
21+
disposable.dispose();
22+
if (e.exitCode !== 0) {
23+
reject(`ERROR: ${e.execution.task.name} failed with code ${e.exitCode}`);
24+
}
25+
resolve(`${name}: shell task completed successfully.`);
26+
}
27+
});
28+
});
29+
}
30+
31+
/**
32+
* Spawn a command as a `Promise`
33+
* @param cmd command to execute
34+
* @param args arguments to pass to the command
35+
* @param options child_process.spawn options
36+
* @param input any input to pass to stdin
37+
* @param ignoreExitCode ignore the exit code of the process and `resolve` the promise
38+
* @returns Tuple[string, string] `[stdout, stderr]`. By default will `reject` if exit code is non-zero.
39+
*/
40+
export async function spawnAsPromise(
41+
cmd: string,
42+
args: ReadonlyArray<string> | undefined,
43+
options?: cp.SpawnOptions | undefined,
44+
input?: string | undefined,
45+
ignoreExitCode?: boolean
46+
) {
47+
return new Promise<[string, string]>((resolve, reject) => {
48+
let stdout = '';
49+
let stderr = '';
50+
const child = cp.spawn(cmd, args, options);
51+
child.stdout.on('data', data => {
52+
stdout += data;
53+
});
54+
child.stderr.on('data', data => {
55+
stderr += data;
56+
});
57+
child.on('close', code => {
58+
if (ignoreExitCode || code === 0) {
59+
resolve([stdout, stderr]);
60+
} else {
61+
reject([stdout, stderr]);
62+
}
63+
});
64+
child.on('error', err => {
65+
reject(err.toString());
66+
});
67+
68+
if (input) {
69+
child.stdin.write(input);
70+
child.stdin.end();
71+
}
72+
});
73+
}

0 commit comments

Comments
 (0)