From 3daed0f644674a7a7a4510c80d414a28d8c758df Mon Sep 17 00:00:00 2001 From: gnikit <giannis.nikiteas@gmail.com> Date: Mon, 5 Jun 2023 13:05:24 +0100 Subject: [PATCH 1/3] feat: add ms-python API --- src/util/ms-python-api/jupyter/types.ts | 50 ++++ src/util/ms-python-api/types.ts | 356 ++++++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 src/util/ms-python-api/jupyter/types.ts create mode 100644 src/util/ms-python-api/types.ts diff --git a/src/util/ms-python-api/jupyter/types.ts b/src/util/ms-python-api/jupyter/types.ts new file mode 100644 index 00000000..cdaf01cd --- /dev/null +++ b/src/util/ms-python-api/jupyter/types.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem } from 'vscode'; + +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick( + item: QuickPickItem, + backEnabled: boolean + ): Promise<JupyterServerUriHandle | 'back' | undefined>; + getServerUri(handle: JupyterServerUriHandle): Promise<IJupyterServerUri>; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise<IDataFrameInfo>; + getAllRows(): Promise<IRowsResponse>; + getRows(start: number, end: number): Promise<IRowsResponse>; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; diff --git a/src/util/ms-python-api/types.ts b/src/util/ms-python-api/types.ts new file mode 100644 index 00000000..1cb426d5 --- /dev/null +++ b/src/util/ms-python-api/types.ts @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; + +import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ + +export interface IExtensionApi { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + * @type {Promise<void>} + * @memberof IExtensionApi + */ + ready: Promise<void>; + jupyter: { + registerHooks(): void; + }; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param {string} host + * @param {number} port + * @param {boolean} [waitUntilDebuggerAttaches=true] + * @returns {Promise<string[]>} + */ + getRemoteLauncherCommand( + host: string, + port: number, + waitUntilDebuggerAttaches: boolean + ): Promise<string[]>; + + /** + * Gets the path to the debugger package used by the extension. + * @returns {Promise<string>} + */ + getDebuggerPackagePath(): Promise<string | undefined>; + }; + + datascience: { + /** + * Launches Data Viewer component. + * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param {string} title Data Viewer title + */ + showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>; + /** + * Registers a remote server provider component that's used to pick remote jupyter server URIs + * @param serverProvider object called back when picking jupyter server URI + */ + registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource + ): Promise<void>; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event<ActiveEnvironmentPathChangeEvent>; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * 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. + */ + readonly onDidChangeEnvironments: Event<EnvironmentsChangeEvent>; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise<void>; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string + ): Promise<ResolvedEnvironment | undefined>; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event<EnvironmentVariablesChangeEvent>; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Workspace folder the environment changed for. + */ + readonly resource: WorkspaceFolder | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; From 90f4cf2b2e73ef3f6e87177b1dc6695f4d54d15d Mon Sep 17 00:00:00 2001 From: gnikit <giannis.nikiteas@gmail.com> Date: Sun, 16 Feb 2025 20:39:56 +0000 Subject: [PATCH 2/3] feat: adds ability to detect Python virtual envs --- package-lock.json | 109 ++++++++++++++++++++++++ package.json | 1 + scripts/get_pip_bin_dir.py | 21 +++++ src/format/provider.ts | 2 +- src/lint/provider.ts | 3 +- src/lsp/client.ts | 2 +- src/util/ms-python-api/jupyter/types.ts | 2 - src/util/python.ts | 82 ++++++++++++++++++ src/util/shell.ts | 73 ++++++++++++++++ src/util/tools.ts | 82 +----------------- test/integration/linter.test.ts | 3 +- test/unitTest/shell.test.ts | 53 ++++++++++++ test/unitTest/tools.test.ts | 49 +---------- webpack.config.js | 6 ++ 14 files changed, 352 insertions(+), 136 deletions(-) create mode 100644 scripts/get_pip_bin_dir.py create mode 100644 src/util/python.ts create mode 100644 src/util/shell.ts create mode 100644 test/unitTest/shell.test.ts diff --git a/package-lock.json b/package-lock.json index 742723b6..0fb3c235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@typescript-eslint/parser": "^8.24.0", "@vscode/test-electron": "^2.4.1", "c8": "^10.1.3", + "copy-webpack-plugin": "^12.0.2", "eslint": "^9.20.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsdoc": "^50.2.2", @@ -731,6 +732,19 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -2763,6 +2777,31 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -4390,6 +4429,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7282,6 +7365,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -8298,6 +8394,19 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/slashes": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", diff --git a/package.json b/package.json index e0bf6d06..2da9fd8e 100644 --- a/package.json +++ b/package.json @@ -761,6 +761,7 @@ "@typescript-eslint/parser": "^8.24.0", "@vscode/test-electron": "^2.4.1", "c8": "^10.1.3", + "copy-webpack-plugin": "^12.0.2", "eslint": "^9.20.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsdoc": "^50.2.2", diff --git a/scripts/get_pip_bin_dir.py b/scripts/get_pip_bin_dir.py new file mode 100644 index 00000000..4aec16ae --- /dev/null +++ b/scripts/get_pip_bin_dir.py @@ -0,0 +1,21 @@ +import os +import sysconfig + + +def get_install_bin_dir() -> str: + """Get the path to the pip install directory for scripts (bin, Scripts). + Works for virtualenv, conda, and system installs. + + Returns + ------- + str + Path to the pip install directory for scripts (bin, Scripts). + """ + if ("VIRTUAL_ENV" in os.environ) or ("CONDA_PREFIX" in os.environ): + return sysconfig.get_path("scripts") + else: + return sysconfig.get_path("scripts", f"{os.name}_user") + + +if __name__ == "__main__": + print(get_install_bin_dir()) diff --git a/src/format/provider.ts b/src/format/provider.ts index e8bae6b6..bcd15cce 100644 --- a/src/format/provider.ts +++ b/src/format/provider.ts @@ -6,12 +6,12 @@ import * as vscode from 'vscode'; import which from 'which'; import { Logger } from '../services/logging'; +import { spawnAsPromise } from '../util/shell'; import { FORMATTERS, EXTENSION_ID, promptForMissingTool, getWholeFileRange, - spawnAsPromise, pathRelToAbs, } from '../util/tools'; diff --git a/src/lint/provider.ts b/src/lint/provider.ts index c3a071d7..ff68fae1 100644 --- a/src/lint/provider.ts +++ b/src/lint/provider.ts @@ -21,14 +21,13 @@ import { import { Logger } from '../services/logging'; import { GlobPaths } from '../util/glob-paths'; import { arraysEqual } from '../util/helper'; +import { spawnAsPromise, shellTask } from '../util/shell'; import { EXTENSION_ID, resolveVariables, promptForMissingTool, isFreeForm, - spawnAsPromise, isFortran, - shellTask, } from '../util/tools'; import { GNULinter, GNUModernLinter, IntelLinter, LFortranLinter, NAGLinter } from './compilers'; diff --git a/src/lsp/client.ts b/src/lsp/client.ts index 5789f2d8..3919769e 100644 --- a/src/lsp/client.ts +++ b/src/lsp/client.ts @@ -10,13 +10,13 @@ import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-lan import { RestartLS } from '../commands/commands'; import { Logger } from '../services/logging'; +import { pipInstall } from '../util/python'; import { EXTENSION_ID, FortranDocumentSelector, LS_NAME, isFortran, getOuterMostWorkspaceFolder, - pipInstall, resolveVariables, } from '../util/tools'; diff --git a/src/util/ms-python-api/jupyter/types.ts b/src/util/ms-python-api/jupyter/types.ts index cdaf01cd..c7fbb6a6 100644 --- a/src/util/ms-python-api/jupyter/types.ts +++ b/src/util/ms-python-api/jupyter/types.ts @@ -9,7 +9,6 @@ interface IJupyterServerUri { baseUrl: string; token: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any authorizationHeader: any; // JSON object for authorization header. expiration?: Date; // Date/time when header expires and should be refreshed. displayName: string; @@ -46,5 +45,4 @@ enum ColumnType { Bool = 'bool', } -// eslint-disable-next-line @typescript-eslint/no-explicit-any type IRowsResponse = any[]; diff --git a/src/util/python.ts b/src/util/python.ts new file mode 100644 index 00000000..c60012a9 --- /dev/null +++ b/src/util/python.ts @@ -0,0 +1,82 @@ +'use strict'; + +import * as path from 'path'; + +import { extensions, Uri } from 'vscode'; + +import { IExtensionApi, ResolvedEnvironment } from './ms-python-api/types'; +import { shellTask, spawnAsPromise } from './shell'; + +/** + * Get Python path from the workspace or the system + * + * @param resource file, folder or workspace to search for python + * @returns string with path to python + */ +export async function getPythonPath(resource?: Uri): Promise<string> { + const pythonEnv = await getPythonEnvMS(resource); + if (pythonEnv) { + return pythonEnv.path; + } + return process.platform === 'win32' ? 'python' : 'python3'; +} + +/** + * Get path that pip installs binaries into. + * Useful, for when the path is not in the PATH environment variable. + * + * @param resource file, folder or workspace + * @returns string with path to pip + */ +export async function getPipBinDir(resource?: Uri): Promise<string> { + const py = await getPythonPath(resource); + const script = path.join(__dirname, 'scripts', 'get_pip_bin_dir.py'); + const [stdout, stderr] = await spawnAsPromise(py, [script]); + if (stderr) { + throw new Error(stderr); + } + return stdout.trim(); +} + +/** + * A wrapper around a call to `pip` for installing external tools. + * Does not explicitly check if `pip` is installed. + * + * @param pyPackage name of python package in PyPi + */ +export async function pipInstall(pyPackage: string): Promise<string> { + const py = await getPythonPath(); + const args = ['-m', 'pip', 'install', '--user', '--upgrade', pyPackage]; + return await shellTask(py, args, `pip: ${pyPackage}`); +} + +/** + * Get the active Python environment, if any, via the ms-python.python + * extension API. + * + * @param resource file/folder/workspace Uri or undefined + * @returns Path to the active Python environment or undefined + */ +export async function getPythonEnvMS( + resource?: Uri | undefined +): Promise<ResolvedEnvironment | undefined> { + try { + const extension = extensions.getExtension('ms-python.python'); + if (!extension) { + return undefined; // extension not installed + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: IExtensionApi = extension.exports as IExtensionApi; + const activeEnv: ResolvedEnvironment = await pythonApi.environments.resolveEnvironment( + pythonApi.environments.getActiveEnvironmentPath(resource) + ); + if (!activeEnv) { + return undefined; // no active environment, unlikely but possible + } + return activeEnv; + } catch (error) { + return undefined; + } +} diff --git a/src/util/shell.ts b/src/util/shell.ts new file mode 100644 index 00000000..a370c66b --- /dev/null +++ b/src/util/shell.ts @@ -0,0 +1,73 @@ +'use strict'; + +import * as cp from 'child_process'; + +import * as vscode from 'vscode'; + +export async function shellTask(command: string, args: string[], name: string): Promise<string> { + const task = new vscode.Task( + { type: 'shell' }, + vscode.TaskScope.Workspace, + name, + 'Modern Fortran', + new vscode.ShellExecution(command, args) + ); + // Temporay fix to https://github.com/microsoft/vscode/issues/157756 + (<vscode.Task>task).definition = { type: 'shell', command: command }; + const execution = await vscode.tasks.executeTask(task); + return await new Promise<string>((resolve, reject) => { + const disposable = vscode.tasks.onDidEndTaskProcess(e => { + if (e.execution === execution) { + disposable.dispose(); + if (e.exitCode !== 0) { + reject(`ERROR: ${e.execution.task.name} failed with code ${e.exitCode}`); + } + resolve(`${name}: shell task completed successfully.`); + } + }); + }); +} + +/** + * Spawn a command as a `Promise` + * @param cmd command to execute + * @param args arguments to pass to the command + * @param options child_process.spawn options + * @param input any input to pass to stdin + * @param ignoreExitCode ignore the exit code of the process and `resolve` the promise + * @returns Tuple[string, string] `[stdout, stderr]`. By default will `reject` if exit code is non-zero. + */ +export async function spawnAsPromise( + cmd: string, + args: ReadonlyArray<string> | undefined, + options?: cp.SpawnOptions | undefined, + input?: string | undefined, + ignoreExitCode?: boolean +) { + return new Promise<[string, string]>((resolve, reject) => { + let stdout = ''; + let stderr = ''; + const child = cp.spawn(cmd, args, options); + child.stdout.on('data', data => { + stdout += data; + }); + child.stderr.on('data', data => { + stderr += data; + }); + child.on('close', code => { + if (ignoreExitCode || code === 0) { + resolve([stdout, stderr]); + } else { + reject([stdout, stderr]); + } + }); + child.on('error', err => { + reject(err.toString()); + }); + + if (input) { + child.stdin.write(input); + child.stdin.end(); + } + }); +} diff --git a/src/util/tools.ts b/src/util/tools.ts index 87df97cd..04f126ae 100644 --- a/src/util/tools.ts +++ b/src/util/tools.ts @@ -1,11 +1,11 @@ import * as assert from 'assert'; -import * as cp from 'child_process'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import { isString, isArrayOfString } from './helper'; +import { pipInstall } from './python'; export const LS_NAME = 'fortls'; export const EXTENSION_ID = 'fortran'; @@ -137,42 +137,6 @@ export async function promptForMissingTool( }); } -/** - * A wrapper around a call to `pip` for installing external tools. - * Does not explicitly check if `pip` is installed. - * - * @param pyPackage name of python package in PyPi - */ -export async function pipInstall(pyPackage: string): Promise<string> { - const py = 'python3'; // Fetches the top-most python in the Shell - const args = ['-m', 'pip', 'install', '--user', '--upgrade', pyPackage]; - return await shellTask(py, args, `pip: ${pyPackage}`); -} - -export async function shellTask(command: string, args: string[], name: string): Promise<string> { - const task = new vscode.Task( - { type: 'shell' }, - vscode.TaskScope.Workspace, - name, - 'Modern Fortran', - new vscode.ShellExecution(command, args) - ); - // Temporay fix to https://github.com/microsoft/vscode/issues/157756 - (<vscode.Task>task).definition = { type: 'shell', command: command }; - const execution = await vscode.tasks.executeTask(task); - return await new Promise<string>((resolve, reject) => { - const disposable = vscode.tasks.onDidEndTaskProcess(e => { - if (e.execution === execution) { - disposable.dispose(); - if (e.exitCode !== 0) { - reject(`ERROR: ${e.execution.task.name} failed with code ${e.exitCode}`); - } - resolve(`${name}: shell task completed successfully.`); - } - }); - }); -} - /** * Resolve VSCode internal variables `workspaceFolder`, `env`, `config`, etc. * @@ -286,47 +250,3 @@ export function pathRelToAbs(relPath: string, uri: vscode.Uri): string | undefin export function getWholeFileRange(document: vscode.TextDocument): vscode.Range { return new vscode.Range(0, 0, document.lineCount, 0); } - -/** - * Spawn a command as a `Promise` - * @param cmd command to execute - * @param args arguments to pass to the command - * @param options child_process.spawn options - * @param input any input to pass to stdin - * @param ignoreExitCode ignore the exit code of the process and `resolve` the promise - * @returns Tuple[string, string] `[stdout, stderr]`. By default will `reject` if exit code is non-zero. - */ -export async function spawnAsPromise( - cmd: string, - args: ReadonlyArray<string> | undefined, - options?: cp.SpawnOptions | undefined, - input?: string | undefined, - ignoreExitCode?: boolean -) { - return new Promise<[string, string]>((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const child = cp.spawn(cmd, args, options); - child.stdout.on('data', data => { - stdout += data; - }); - child.stderr.on('data', data => { - stderr += data; - }); - child.on('close', code => { - if (ignoreExitCode || code === 0) { - resolve([stdout, stderr]); - } else { - reject([stdout, stderr]); - } - }); - child.on('error', err => { - reject(err.toString()); - }); - - if (input) { - child.stdin.write(input); - child.stdin.end(); - } - }); -} diff --git a/test/integration/linter.test.ts b/test/integration/linter.test.ts index 2cb2106b..43dfacff 100644 --- a/test/integration/linter.test.ts +++ b/test/integration/linter.test.ts @@ -29,7 +29,8 @@ import { } from '../../src/lint/compilers'; import { FortranLintingProvider } from '../../src/lint/provider'; import { LogLevel, Logger } from '../../src/services/logging'; -import { EXTENSION_ID, pipInstall } from '../../src/util/tools'; +import { pipInstall } from '../../src/util/python'; +import { EXTENSION_ID } from '../../src/util/tools'; suite('Linter VS Code commands', async () => { let doc: TextDocument; diff --git a/test/unitTest/shell.test.ts b/test/unitTest/shell.test.ts new file mode 100644 index 00000000..bbfa0c91 --- /dev/null +++ b/test/unitTest/shell.test.ts @@ -0,0 +1,53 @@ +import * as assert from 'assert'; +import * as path from 'path'; + +import { shellTask, spawnAsPromise } from '../../src/util/shell'; + +suite('Tools tests', () => { + test('shellTask returns correct output', async () => { + const name = 'pip: fortls'; + const output = await shellTask( + 'python3', + ['-m', 'pip', 'install', '--upgrade', '--force', 'fortls'], + name + ); + assert.strictEqual(output, `${name}: shell task completed successfully.`); + }); + + test('shellTask returns rejected promise', async () => { + const name = 'pip: fortls'; + await assert.rejects(shellTask('python3', ['-m', 'pip', 'install', 'fortls2'], name)); + }); + + test('spawnAsPromise correct stdout, stderr output exit code 0', async () => { + const [stdout, stderr] = await spawnAsPromise('node', [ + path.resolve(__dirname, './exit-code.js'), + ]); + assert.strictEqual(stdout, 'Hello World!'); + assert.strictEqual(stderr, 'No errors'); + }); + + test('spawnAsPromise correct stdout, stderr output exit code 1', async () => { + try { + const [stdout, stderr] = await spawnAsPromise('node', [ + path.resolve(__dirname, './exit-code-err.js'), + ]); + } catch (error) { + const [stdout, stderr] = error; + assert.strictEqual(stdout, 'Hello World!'); + assert.strictEqual(stderr, 'Errors'); + } + }); + + test('spawnAsPromise correct stdout, stderr output exit code 1 with ignoreExitCode', async () => { + const [stdout, stderr] = await spawnAsPromise( + 'node', + [path.resolve(__dirname, './exit-code-err.js')], + undefined, + undefined, + true + ); + assert.strictEqual(stdout, 'Hello World!'); + assert.strictEqual(stderr, 'Errors'); + }); +}); diff --git a/test/unitTest/tools.test.ts b/test/unitTest/tools.test.ts index 7c59a2a7..5940791c 100644 --- a/test/unitTest/tools.test.ts +++ b/test/unitTest/tools.test.ts @@ -3,56 +3,9 @@ import * as path from 'path'; import { Uri } from 'vscode'; -import { shellTask, spawnAsPromise, pathRelToAbs } from '../../src/util/tools'; +import { pathRelToAbs } from '../../src/util/tools'; suite('Tools tests', () => { - test('shellTask returns correct output', async () => { - const name = 'pip: fortls'; - const output = await shellTask( - 'python3', - ['-m', 'pip', 'install', '--upgrade', '--force', 'fortls'], - name - ); - assert.strictEqual(output, `${name}: shell task completed successfully.`); - }); - - test('shellTask returns rejected promise', async () => { - const name = 'pip: fortls'; - await assert.rejects(shellTask('python3', ['-m', 'pip', 'install', 'fortls2'], name)); - }); - - test('spawnAsPromise correct stdout, stderr output exit code 0', async () => { - const [stdout, stderr] = await spawnAsPromise('node', [ - path.resolve(__dirname, './exit-code.js'), - ]); - assert.strictEqual(stdout, 'Hello World!'); - assert.strictEqual(stderr, 'No errors'); - }); - - test('spawnAsPromise correct stdout, stderr output exit code 1', async () => { - try { - const [stdout, stderr] = await spawnAsPromise('node', [ - path.resolve(__dirname, './exit-code-err.js'), - ]); - } catch (error) { - const [stdout, stderr] = error; - assert.strictEqual(stdout, 'Hello World!'); - assert.strictEqual(stderr, 'Errors'); - } - }); - - test('spawnAsPromise correct stdout, stderr output exit code 1 with ignoreExitCode', async () => { - const [stdout, stderr] = await spawnAsPromise( - 'node', - [path.resolve(__dirname, './exit-code-err.js')], - undefined, - undefined, - true - ); - assert.strictEqual(stdout, 'Hello World!'); - assert.strictEqual(stderr, 'Errors'); - }); - test('Resolve local paths: undefined', () => { const root = Uri.parse('/home/user/project'); const absPath = pathRelToAbs('./sample.f90', root); diff --git a/webpack.config.js b/webpack.config.js index 773a2cbf..2edcdeac 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,6 +4,7 @@ const path = require('path'); const webpack = require('webpack'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); /**@type {import('webpack').Configuration}*/ const config = { @@ -25,6 +26,11 @@ const config = { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader extensions: ['.ts', '.js'], }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [{ from: 'scripts/get_pip_bin_dir.py', to: 'scripts/get_pip_bin_dir.py' }], + }), + ], module: { rules: [ { From 992904ea78eaf40b23d31bea28a6dd7269ef9ce1 Mon Sep 17 00:00:00 2001 From: gnikit <giannis.nikiteas@gmail.com> Date: Mon, 6 Nov 2023 22:54:14 +0000 Subject: [PATCH 3/3] refactor: half-baked Python config implementation --- package.json | 2 +- scripts/get_console_script.py | 30 +++++++ scripts/mod_in_env.py | 8 ++ src/lsp/client.ts | 33 +++++++- src/util/python-config.ts | 8 ++ src/util/python.ts | 150 +++++++++++++++++++--------------- src/util/tools.ts | 5 +- webpack.config.js | 5 +- 8 files changed, 167 insertions(+), 74 deletions(-) create mode 100644 scripts/get_console_script.py create mode 100644 scripts/mod_in_env.py create mode 100644 src/util/python-config.ts diff --git a/package.json b/package.json index 2da9fd8e..4548876e 100644 --- a/package.json +++ b/package.json @@ -394,7 +394,7 @@ "properties": { "fortran.fortls.path": { "type": "string", - "default": "fortls", + "default": "", "markdownDescription": "Path to the Fortran language server (`fortls`).", "order": 10 }, diff --git a/scripts/get_console_script.py b/scripts/get_console_script.py new file mode 100644 index 00000000..6bf19445 --- /dev/null +++ b/scripts/get_console_script.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sys +import pkg_resources + + +def get_console_script_path(package_name: str, script_name: str) -> str | None: + # """ + # Get the absolute path of a console script from a package. + + # @param package_name: The name of the package. + # @param script_name: The name of the console script. + # @return The absolute path of the console script. + # """ + try: + # Get the distribution object for the package + dist = pkg_resources.get_distribution(package_name) + # Get the entry point for the console script + entry_point = dist.get_entry_info("console_scripts", script_name) + # Get the path to the script + script_path = entry_point.module_name.split(":")[0] + # Return the absolute path of the script + return pkg_resources.resource_filename(dist, script_path) + except Exception as e: + # Handle any exceptions that occur + print(f"Error: {e}") + return None + + +print(get_console_script_path(sys.argv[1], sys.argv[2])) diff --git a/scripts/mod_in_env.py b/scripts/mod_in_env.py new file mode 100644 index 00000000..a5bdb11c --- /dev/null +++ b/scripts/mod_in_env.py @@ -0,0 +1,8 @@ +#! /usr/bin/env python3 + +import sys +import pkg_resources + +# If not present, fails with a DistributionNotFound exception +if pkg_resources.get_distribution(str(sys.argv[1])).version: + exit(0) diff --git a/src/lsp/client.ts b/src/lsp/client.ts index 3919769e..1531d94f 100644 --- a/src/lsp/client.ts +++ b/src/lsp/client.ts @@ -10,7 +10,7 @@ import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-lan import { RestartLS } from '../commands/commands'; import { Logger } from '../services/logging'; -import { pipInstall } from '../util/python'; +import { Python } from '../util/python'; import { EXTENSION_ID, FortranDocumentSelector, @@ -26,11 +26,16 @@ import { export const clients: Map<string, LanguageClient> = new Map(); export class FortlsClient { + private readonly python: Python; + private readonly config: vscode.WorkspaceConfiguration; + constructor( private logger: Logger, private context?: vscode.ExtensionContext ) { this.logger.debug('[lsp.client] Fortran Language Server -- constructor'); + this.python = new Python(); + this.config = workspace.getConfiguration(EXTENSION_ID); // if context is present if (context !== undefined) { @@ -316,6 +321,7 @@ export class FortlsClient { const ls = await this.fortlsPath(); // Check for version, if this fails fortls provided is invalid + const pipBin: string = await this.python.getPipBinDir(); const results = spawnSync(ls, ['--version']); const msg = `It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more. For a full list of features the language server adds see: https://fortls.fortran-lang.org`; @@ -326,7 +332,7 @@ export class FortlsClient { if (opt === 'Install') { try { this.logger.info(`[lsp.client] Downloading ${LS_NAME}`); - const msg = await pipInstall(LS_NAME); + const msg = await this.python.pipInstall(LS_NAME); window.showInformationMessage(msg); this.logger.info(`[lsp.client] ${LS_NAME} installed`); resolve(false); @@ -378,12 +384,21 @@ export class FortlsClient { const root = folder ? getOuterMostWorkspaceFolder(folder).uri : vscode.Uri.parse(os.homedir()); const config = workspace.getConfiguration(EXTENSION_ID); - let executablePath = resolveVariables(config.get<string>('fortls.path')); + // TODO: make the default value undefined, make windows use fortls.exe + // get the full path of the Python bin dir, check if file exists + // else we are running a script with a relative path (verify this is the case) + let executablePath = config.get<string>('fortls.path'); + if (!executablePath || this.isFortlsPathDefault(executablePath)) { + executablePath = 'fortls' + (os.platform() === 'win32' ? '.exe' : ''); + executablePath = path.join(await this.python.getPipBinDir(), executablePath); + } else { + executablePath = resolveVariables(executablePath); + } // The path can be resolved as a relative path if: // 1. it does not have the default value `fortls` AND // 2. is not an absolute path - if (executablePath !== 'fortls' && !path.isAbsolute(executablePath)) { + if (!path.isAbsolute(executablePath)) { this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.fsPath}`); executablePath = path.join(root.fsPath, executablePath); } @@ -391,6 +406,16 @@ export class FortlsClient { return executablePath; } + private async isFortlsPathDefault(path: string): Promise<boolean> { + if (path === 'fortls') { + return true; + } + if (os.platform() === 'win32' && path === 'fortls.exe') { + return true; + } + return false; + } + /** * Restart the language server */ diff --git a/src/util/python-config.ts b/src/util/python-config.ts new file mode 100644 index 00000000..7935bc24 --- /dev/null +++ b/src/util/python-config.ts @@ -0,0 +1,8 @@ +'use strict'; + +import { Uri } from 'vscode'; + +import { Python } from './python'; + +// TODO: implement interface and config +export class PythonConfiguration {} diff --git a/src/util/python.ts b/src/util/python.ts index c60012a9..3222df0b 100644 --- a/src/util/python.ts +++ b/src/util/python.ts @@ -1,5 +1,3 @@ -'use strict'; - import * as path from 'path'; import { extensions, Uri } from 'vscode'; @@ -7,76 +5,96 @@ import { extensions, Uri } from 'vscode'; import { IExtensionApi, ResolvedEnvironment } from './ms-python-api/types'; import { shellTask, spawnAsPromise } from './shell'; -/** - * Get Python path from the workspace or the system - * - * @param resource file, folder or workspace to search for python - * @returns string with path to python - */ -export async function getPythonPath(resource?: Uri): Promise<string> { - const pythonEnv = await getPythonEnvMS(resource); - if (pythonEnv) { - return pythonEnv.path; - } - return process.platform === 'win32' ? 'python' : 'python3'; -} +export class Python { + public readonly path: Promise<string>; + private usingMSPython = false; + private pythonEnvMS: ResolvedEnvironment | undefined; -/** - * Get path that pip installs binaries into. - * Useful, for when the path is not in the PATH environment variable. - * - * @param resource file, folder or workspace - * @returns string with path to pip - */ -export async function getPipBinDir(resource?: Uri): Promise<string> { - const py = await getPythonPath(resource); - const script = path.join(__dirname, 'scripts', 'get_pip_bin_dir.py'); - const [stdout, stderr] = await spawnAsPromise(py, [script]); - if (stderr) { - throw new Error(stderr); + constructor(resource?: Uri) { + this.path = this.getPythonPath(resource); } - return stdout.trim(); -} -/** - * A wrapper around a call to `pip` for installing external tools. - * Does not explicitly check if `pip` is installed. - * - * @param pyPackage name of python package in PyPi - */ -export async function pipInstall(pyPackage: string): Promise<string> { - const py = await getPythonPath(); - const args = ['-m', 'pip', 'install', '--user', '--upgrade', pyPackage]; - return await shellTask(py, args, `pip: ${pyPackage}`); -} + /** + * Get the path to the active Python interpreter. + * + * @returns The path to the active Python interpreter. + */ + public async getPythonPath(resource?: Uri): Promise<string> { + const pythonEnv = await this.getPythonEnvMS(resource); + if (pythonEnv) { + this.usingMSPython = true; + return pythonEnv.path; + } + return process.platform === 'win32' ? 'python' : 'python3'; + } -/** - * Get the active Python environment, if any, via the ms-python.python - * extension API. - * - * @param resource file/folder/workspace Uri or undefined - * @returns Path to the active Python environment or undefined - */ -export async function getPythonEnvMS( - resource?: Uri | undefined -): Promise<ResolvedEnvironment | undefined> { - try { - const extension = extensions.getExtension('ms-python.python'); - if (!extension) { - return undefined; // extension not installed + /** + * Get the path to the directory where pip installs binaries. + * + * @returns The path to the directory where pip installs binaries. + */ + public async getPipBinDir(): Promise<string> { + const py = await this.path; + const script = path.join(__dirname, 'scripts', 'get_pip_bin_dir.py'); + const [stdout, stderr] = await spawnAsPromise(py, [script]); + if (stderr) { + throw new Error(stderr); } - if (!extension.isActive) { - await extension.activate(); + return stdout.trim(); + } + + /** + * Install a Python package using pip. + * + * @param packageName The name of the package to install. + * @returns The output of the pip command. + */ + public async pipInstall(packageName: string): Promise<string> { + const py = await this.path; + const args = ['-m', 'pip', 'install', '--user', '--upgrade', packageName]; + return await shellTask(py, args, `pip: ${packageName}`); + } + + /** + * Get the active Python environment, if any, via the ms-python.python + * extension API. + * + * @returns The active Python environment, or undefined if there is none. + */ + public async getPythonEnvMS(resource?: Uri): Promise<ResolvedEnvironment | undefined> { + try { + const extension = extensions.getExtension('ms-python.python'); + if (!extension) { + return undefined; // extension not installed + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: IExtensionApi = extension.exports as IExtensionApi; + const activeEnv: ResolvedEnvironment = await pythonApi.environments.resolveEnvironment( + pythonApi.environments.getActiveEnvironmentPath(resource) + ); + if (!activeEnv) { + return undefined; // no active environment, unlikely but possible + } + this.pythonEnvMS = activeEnv; + return activeEnv; + } catch (error) { + return undefined; } - const pythonApi: IExtensionApi = extension.exports as IExtensionApi; - const activeEnv: ResolvedEnvironment = await pythonApi.environments.resolveEnvironment( - pythonApi.environments.getActiveEnvironmentPath(resource) - ); - if (!activeEnv) { - return undefined; // no active environment, unlikely but possible + } + + public async isInstalled(packageName: string): Promise<boolean> { + const py = await this.path; + const script = path.join(__dirname, 'scripts', 'mod_in_env.py'); + try { + const [_, stderr] = await spawnAsPromise(py, [script, packageName]); + if (stderr) { + return false; + } + return true; + } catch (error) { + return false; } - return activeEnv; - } catch (error) { - return undefined; } } diff --git a/src/util/tools.ts b/src/util/tools.ts index 04f126ae..09704d7f 100644 --- a/src/util/tools.ts +++ b/src/util/tools.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { isString, isArrayOfString } from './helper'; -import { pipInstall } from './python'; +import { Python } from './python'; export const LS_NAME = 'fortls'; export const EXTENSION_ID = 'fortran'; @@ -119,7 +119,8 @@ export async function promptForMissingTool( if (selected === 'Install') { if (toolType === 'Python') { try { - const inst_msg = await pipInstall(tool); + const python = new Python(); + const inst_msg = await python.pipInstall(tool); vscode.window.showInformationMessage(inst_msg); } catch (error) { vscode.window.showErrorMessage(error); diff --git a/webpack.config.js b/webpack.config.js index 2edcdeac..ad2949c5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -28,7 +28,10 @@ const config = { }, plugins: [ new CopyWebpackPlugin({ - patterns: [{ from: 'scripts/get_pip_bin_dir.py', to: 'scripts/get_pip_bin_dir.py' }], + patterns: [ + { from: 'scripts/get_pip_bin_dir.py', to: 'scripts/get_pip_bin_dir.py' }, + { from: 'scripts/mod_in_env.py', to: 'scripts/mod_in_env.py' }, + ], }), ], module: {