Skip to content

Commit 992904e

Browse files
committed
refactor: half-baked Python config implementation
1 parent 90f4cf2 commit 992904e

File tree

8 files changed

+167
-74
lines changed

8 files changed

+167
-74
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@
394394
"properties": {
395395
"fortran.fortls.path": {
396396
"type": "string",
397-
"default": "fortls",
397+
"default": "",
398398
"markdownDescription": "Path to the Fortran language server (`fortls`).",
399399
"order": 10
400400
},

scripts/get_console_script.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
import pkg_resources
5+
6+
7+
def get_console_script_path(package_name: str, script_name: str) -> str | None:
8+
# """
9+
# Get the absolute path of a console script from a package.
10+
11+
# @param package_name: The name of the package.
12+
# @param script_name: The name of the console script.
13+
# @return The absolute path of the console script.
14+
# """
15+
try:
16+
# Get the distribution object for the package
17+
dist = pkg_resources.get_distribution(package_name)
18+
# Get the entry point for the console script
19+
entry_point = dist.get_entry_info("console_scripts", script_name)
20+
# Get the path to the script
21+
script_path = entry_point.module_name.split(":")[0]
22+
# Return the absolute path of the script
23+
return pkg_resources.resource_filename(dist, script_path)
24+
except Exception as e:
25+
# Handle any exceptions that occur
26+
print(f"Error: {e}")
27+
return None
28+
29+
30+
print(get_console_script_path(sys.argv[1], sys.argv[2]))

scripts/mod_in_env.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#! /usr/bin/env python3
2+
3+
import sys
4+
import pkg_resources
5+
6+
# If not present, fails with a DistributionNotFound exception
7+
if pkg_resources.get_distribution(str(sys.argv[1])).version:
8+
exit(0)

src/lsp/client.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ 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';
13+
import { Python } from '../util/python';
1414
import {
1515
EXTENSION_ID,
1616
FortranDocumentSelector,
@@ -26,11 +26,16 @@ import {
2626
export const clients: Map<string, LanguageClient> = new Map();
2727

2828
export class FortlsClient {
29+
private readonly python: Python;
30+
private readonly config: vscode.WorkspaceConfiguration;
31+
2932
constructor(
3033
private logger: Logger,
3134
private context?: vscode.ExtensionContext
3235
) {
3336
this.logger.debug('[lsp.client] Fortran Language Server -- constructor');
37+
this.python = new Python();
38+
this.config = workspace.getConfiguration(EXTENSION_ID);
3439

3540
// if context is present
3641
if (context !== undefined) {
@@ -316,6 +321,7 @@ export class FortlsClient {
316321
const ls = await this.fortlsPath();
317322

318323
// Check for version, if this fails fortls provided is invalid
324+
const pipBin: string = await this.python.getPipBinDir();
319325
const results = spawnSync(ls, ['--version']);
320326
const msg = `It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more.
321327
For a full list of features the language server adds see: https://fortls.fortran-lang.org`;
@@ -326,7 +332,7 @@ export class FortlsClient {
326332
if (opt === 'Install') {
327333
try {
328334
this.logger.info(`[lsp.client] Downloading ${LS_NAME}`);
329-
const msg = await pipInstall(LS_NAME);
335+
const msg = await this.python.pipInstall(LS_NAME);
330336
window.showInformationMessage(msg);
331337
this.logger.info(`[lsp.client] ${LS_NAME} installed`);
332338
resolve(false);
@@ -378,19 +384,38 @@ export class FortlsClient {
378384
const root = folder ? getOuterMostWorkspaceFolder(folder).uri : vscode.Uri.parse(os.homedir());
379385

380386
const config = workspace.getConfiguration(EXTENSION_ID);
381-
let executablePath = resolveVariables(config.get<string>('fortls.path'));
387+
// TODO: make the default value undefined, make windows use fortls.exe
388+
// get the full path of the Python bin dir, check if file exists
389+
// else we are running a script with a relative path (verify this is the case)
390+
let executablePath = config.get<string>('fortls.path');
391+
if (!executablePath || this.isFortlsPathDefault(executablePath)) {
392+
executablePath = 'fortls' + (os.platform() === 'win32' ? '.exe' : '');
393+
executablePath = path.join(await this.python.getPipBinDir(), executablePath);
394+
} else {
395+
executablePath = resolveVariables(executablePath);
396+
}
382397

383398
// The path can be resolved as a relative path if:
384399
// 1. it does not have the default value `fortls` AND
385400
// 2. is not an absolute path
386-
if (executablePath !== 'fortls' && !path.isAbsolute(executablePath)) {
401+
if (!path.isAbsolute(executablePath)) {
387402
this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.fsPath}`);
388403
executablePath = path.join(root.fsPath, executablePath);
389404
}
390405

391406
return executablePath;
392407
}
393408

409+
private async isFortlsPathDefault(path: string): Promise<boolean> {
410+
if (path === 'fortls') {
411+
return true;
412+
}
413+
if (os.platform() === 'win32' && path === 'fortls.exe') {
414+
return true;
415+
}
416+
return false;
417+
}
418+
394419
/**
395420
* Restart the language server
396421
*/

src/util/python-config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
import { Uri } from 'vscode';
4+
5+
import { Python } from './python';
6+
7+
// TODO: implement interface and config
8+
export class PythonConfiguration {}

src/util/python.ts

+84-66
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,100 @@
1-
'use strict';
2-
31
import * as path from 'path';
42

53
import { extensions, Uri } from 'vscode';
64

75
import { IExtensionApi, ResolvedEnvironment } from './ms-python-api/types';
86
import { shellTask, spawnAsPromise } from './shell';
97

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-
}
8+
export class Python {
9+
public readonly path: Promise<string>;
10+
private usingMSPython = false;
11+
private pythonEnvMS: ResolvedEnvironment | undefined;
2312

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);
13+
constructor(resource?: Uri) {
14+
this.path = this.getPythonPath(resource);
3715
}
38-
return stdout.trim();
39-
}
4016

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-
}
17+
/**
18+
* Get the path to the active Python interpreter.
19+
*
20+
* @returns The path to the active Python interpreter.
21+
*/
22+
public async getPythonPath(resource?: Uri): Promise<string> {
23+
const pythonEnv = await this.getPythonEnvMS(resource);
24+
if (pythonEnv) {
25+
this.usingMSPython = true;
26+
return pythonEnv.path;
27+
}
28+
return process.platform === 'win32' ? 'python' : 'python3';
29+
}
5230

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
31+
/**
32+
* Get the path to the directory where pip installs binaries.
33+
*
34+
* @returns The path to the directory where pip installs binaries.
35+
*/
36+
public async getPipBinDir(): Promise<string> {
37+
const py = await this.path;
38+
const script = path.join(__dirname, 'scripts', 'get_pip_bin_dir.py');
39+
const [stdout, stderr] = await spawnAsPromise(py, [script]);
40+
if (stderr) {
41+
throw new Error(stderr);
6742
}
68-
if (!extension.isActive) {
69-
await extension.activate();
43+
return stdout.trim();
44+
}
45+
46+
/**
47+
* Install a Python package using pip.
48+
*
49+
* @param packageName The name of the package to install.
50+
* @returns The output of the pip command.
51+
*/
52+
public async pipInstall(packageName: string): Promise<string> {
53+
const py = await this.path;
54+
const args = ['-m', 'pip', 'install', '--user', '--upgrade', packageName];
55+
return await shellTask(py, args, `pip: ${packageName}`);
56+
}
57+
58+
/**
59+
* Get the active Python environment, if any, via the ms-python.python
60+
* extension API.
61+
*
62+
* @returns The active Python environment, or undefined if there is none.
63+
*/
64+
public async getPythonEnvMS(resource?: Uri): Promise<ResolvedEnvironment | undefined> {
65+
try {
66+
const extension = extensions.getExtension('ms-python.python');
67+
if (!extension) {
68+
return undefined; // extension not installed
69+
}
70+
if (!extension.isActive) {
71+
await extension.activate();
72+
}
73+
const pythonApi: IExtensionApi = extension.exports as IExtensionApi;
74+
const activeEnv: ResolvedEnvironment = await pythonApi.environments.resolveEnvironment(
75+
pythonApi.environments.getActiveEnvironmentPath(resource)
76+
);
77+
if (!activeEnv) {
78+
return undefined; // no active environment, unlikely but possible
79+
}
80+
this.pythonEnvMS = activeEnv;
81+
return activeEnv;
82+
} catch (error) {
83+
return undefined;
7084
}
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
85+
}
86+
87+
public async isInstalled(packageName: string): Promise<boolean> {
88+
const py = await this.path;
89+
const script = path.join(__dirname, 'scripts', 'mod_in_env.py');
90+
try {
91+
const [_, stderr] = await spawnAsPromise(py, [script, packageName]);
92+
if (stderr) {
93+
return false;
94+
}
95+
return true;
96+
} catch (error) {
97+
return false;
7798
}
78-
return activeEnv;
79-
} catch (error) {
80-
return undefined;
8199
}
82100
}

src/util/tools.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from 'path';
55
import * as vscode from 'vscode';
66

77
import { isString, isArrayOfString } from './helper';
8-
import { pipInstall } from './python';
8+
import { Python } from './python';
99

1010
export const LS_NAME = 'fortls';
1111
export const EXTENSION_ID = 'fortran';
@@ -119,7 +119,8 @@ export async function promptForMissingTool(
119119
if (selected === 'Install') {
120120
if (toolType === 'Python') {
121121
try {
122-
const inst_msg = await pipInstall(tool);
122+
const python = new Python();
123+
const inst_msg = await python.pipInstall(tool);
123124
vscode.window.showInformationMessage(inst_msg);
124125
} catch (error) {
125126
vscode.window.showErrorMessage(error);

webpack.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ const config = {
2828
},
2929
plugins: [
3030
new CopyWebpackPlugin({
31-
patterns: [{ from: 'scripts/get_pip_bin_dir.py', to: 'scripts/get_pip_bin_dir.py' }],
31+
patterns: [
32+
{ from: 'scripts/get_pip_bin_dir.py', to: 'scripts/get_pip_bin_dir.py' },
33+
{ from: 'scripts/mod_in_env.py', to: 'scripts/mod_in_env.py' },
34+
],
3235
}),
3336
],
3437
module: {

0 commit comments

Comments
 (0)