Skip to content

Commit

Permalink
Support settings of custom project path in Kedro Extension (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
jitu5 authored Jan 9, 2025
1 parent 4bb76b7 commit 2523edf
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 160 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,36 @@ Navigate to DataCatalog:
Clicking on a data node in the flowchart will open the corresponding dataset in the Data Catalog.
![navigation to dataset](assets/viz-vsc-nav-data-node.gif)

### Set Custom Kedro Project Path

You can specify a custom path to your Kedro project root directory in one of two ways:

1. **Using the Command Palette**:
- Press `Cmd` + `Shift` + `P` (on macOS) or `Ctrl` + `Shift` + `P` (on Windows/Linux)
- Type and select `Kedro: Set Project Path`
- Enter the absolute path to your Kedro project root directory
- The extension will validate if it's a valid Kedro project by checking for `pyproject.toml`

2. **Using Settings**:
- Open VS Code Settings (File > Preferences > Settings)
- Search for "Kedro Project Path"
- Enter the absolute path to your Kedro project root directory in the `kedro.kedroProjectPath` setting

The extension will:
- Validate that the provided path contains a valid Kedro project
- Add the project folder to your workspace if it's not already included
- Use this path as the root directory for all Kedro-related features

**Note:** The project path must point to a directory containing a valid Kedro project with a `pyproject.toml` file that includes the `[tool.kedro]` section.

Example:
```
{
"kedro.kedroProjectPath": "/absolute/path/to/your/kedro-project"
}
```

![Set Kedro project path](assets/kedro-project-path.gif)

## Settings
### Change Configuration Environment
Expand All @@ -108,8 +137,6 @@ Click `Output` and select `Kedro` from the dropdown list. It may gives you some
Hit `Cmd` + `Shift` + `P` to open the VSCode command, look for `kedro: restart server` in case it's panic.

## Assumptions
### Single Kedro Project
The extension need to identify where is the Kedro project. It assumes the root of the workspace is a Kedro project, i.e. open the project where the `pyproject.toml` is.

### Configure Kedro Environment
Currently, the extension assume the source of configuration is in the `base_env` defined by the config loader (if you didn't speficy, [usually it is `conf/base`](https://docs.kedro.org/en/stable/configuration/configuration_basics.html#configuration-loading)).
Expand Down
Binary file added assets/kedro-project-path.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions bundled/tool/lsp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def _set_project_with_workspace(self):
try:
self.workspace_settings = next(iter(WORKSPACE_SETTINGS.values()))
root_path = pathlib.Path(
self.workspace.root_path
self.workspace_settings.get("kedroProjectPath") or self.workspace.root_path
) # todo: From language server, can we get it from client initialise response instead?
project_metadata = bootstrap_project(root_path)
env = None
Expand Down Expand Up @@ -489,6 +489,7 @@ def _get_global_defaults():
"importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"),
"showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"),
"environment": GLOBAL_SETTINGS.get("environment", ""),
"kedroProjectPath": GLOBAL_SETTINGS.get("kedroProjectPath", ""),
}


Expand Down Expand Up @@ -578,7 +579,9 @@ def get_project_data_from_viz(lsClient):

data = None
try:
load_and_populate_data(Path.cwd())
workspace_settings = next(iter(WORKSPACE_SETTINGS.values()))
kedro_project_path = Path(workspace_settings.get("kedroProjectPath")) or Path.cwd()
load_and_populate_data(kedro_project_path)
data = get_kedro_project_json_data()
return data
except Exception as e:
Expand Down
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@
],
"scope": "machine",
"type": "string"
},
"kedro.kedroProjectPath": {
"default": "",
"description": "Custom path to Kedro project root directory. Please add absolute path to your Kedro project root directory.",
"scope": "resource",
"type": "string"
}
}
},
Expand Down Expand Up @@ -167,6 +173,11 @@
"command": "kedro.showOutputChannel",
"category": "kedro",
"title": "Show logs"
},
{
"command": "kedro.kedroProjectPath",
"title": "Set Project Path",
"category": "kedro"
}
]
},
Expand Down
167 changes: 167 additions & 0 deletions src/common/activationHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import {
selectEnvironment,
executeServerCommand,
executeServerDefinitionCommand,
setKedroProjectPath,
} from './commands';

import * as vscode from 'vscode';
import { registerLogger, traceError, traceLog, traceVerbose } from './log/logging';
import { checkVersion, getInterpreterDetails, onDidChangePythonInterpreter, resolveInterpreter } from './python';
import { sendHeapEventWithMetadata } from './telemetry';
import { restartServer } from './server';
import { checkIfConfigurationChanged, getInterpreterFromSetting } from './settings';
import { loadServerDefaults } from './setup';
import { createStatusBar } from './status_bar';
import { getLSClientTraceLevel, updateKedroVizPanel } from './utilities';
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './vscodeapi';
import KedroVizPanel from '../webview/vizWebView';
import { handleKedroViz } from '../webview/createOrShowKedroVizPanel';
import { LanguageClient } from 'vscode-languageclient/node';

/**
* Runs the language server based on current environment and interpreter settings.
* Returns the updated lsClient reference.
*/
export const runServer = async (
lsClient: LanguageClient | undefined,
selectedEnvironment?: vscode.QuickPickItem,
): Promise<LanguageClient | undefined> => {
const serverInfo = loadServerDefaults();
const serverName = serverInfo.name;
const serverId = serverInfo.module;

const outputChannel = createOutputChannel(serverName);
const interpreter = getInterpreterFromSetting(serverId);
let env: string | undefined = selectedEnvironment?.label;

if (interpreter && interpreter.length > 0) {
if (checkVersion(await resolveInterpreter(interpreter))) {
traceVerbose(`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(' ')}`);
lsClient = await restartServer(serverId, serverName, outputChannel, lsClient, env);
}
return lsClient;
}

const interpreterDetails = await getInterpreterDetails();
console.log('===============DEBUG============');
console.log(interpreterDetails);
console.log('===============DEBUG============');

if (interpreterDetails.path) {
traceVerbose(`Using interpreter from Python extension: ${interpreterDetails.path.join(' ')}`);
lsClient = await restartServer(serverId, serverName, outputChannel, lsClient, env);
return lsClient;
}

traceError(
'Python interpreter missing:\r\n' +
'[Option 1] Select python interpreter using the ms-python.python.\r\n' +
`[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` +
'Please use Python 3.8 or greater.',
);

return lsClient;
};

/**
* Registers commands, events, and sets up logging and status bar.
* Accepts get/set functions for lsClient so that whenever runServer updates it,
* we can update the stored reference as well.
*/
export const registerCommandsAndEvents = (
context: vscode.ExtensionContext,
getLSClient: () => LanguageClient | undefined,
setLSClient: (client: LanguageClient | undefined) => void,
) => {
const serverInfo = loadServerDefaults();
const serverName = serverInfo.name;
const serverId = serverInfo.module;

const outputChannel = createOutputChannel(serverName);

// List of commands
const CMD_RESTART_SERVER = `${serverId}.restart`;
const CMD_SELECT_ENV = `${serverId}.selectEnvironment`;
const CMD_RUN_KEDRO_VIZ = `${serverId}.runKedroViz`;
const CMD_DEFINITION_REQUEST = `${serverId}.sendDefinitionRequest`;
const CMD_SHOW_OUTPUT_CHANNEL = `${serverId}.showOutputChannel`;
const CMD_SET_PROJECT_PATH = `${serverId}.kedroProjectPath`;

(async () => {
// Status Bar
const statusBarItem = await createStatusBar(CMD_SELECT_ENV, serverId);
context.subscriptions.push(statusBarItem);

// Setup logging
context.subscriptions.push(outputChannel, registerLogger(outputChannel));

const changeLogLevel = async (c: vscode.LogLevel, g: vscode.LogLevel) => {
const level = getLSClientTraceLevel(c, g);
await getLSClient()?.setTrace(level);
};

context.subscriptions.push(
outputChannel.onDidChangeLogLevel(async (e) => {
await changeLogLevel(e, vscode.env.logLevel);
}),
vscode.env.onDidChangeLogLevel(async (e) => {
await changeLogLevel(outputChannel.logLevel, e);
}),
);

traceLog(`Name: ${serverInfo.name}`);
traceLog(`Module: ${serverInfo.module}`);
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`);

context.subscriptions.push(
onDidChangePythonInterpreter(async () => {
const newClient = await runServer(getLSClient());
setLSClient(newClient);
}),
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
if (checkIfConfigurationChanged(e, serverId)) {
const newClient = await runServer(getLSClient());
setLSClient(newClient);
}
}),
registerCommand(CMD_RESTART_SERVER, async () => {
const newClient = await runServer(getLSClient());
setLSClient(newClient);
await sendHeapEventWithMetadata(CMD_RESTART_SERVER, context);

// If KedroVizPanel is open, update the data on server restart
if (KedroVizPanel.currentPanel) {
updateKedroVizPanel(getLSClient());
}
}),
registerCommand(CMD_SELECT_ENV, async () => {
const result = await selectEnvironment();
const newClient = await runServer(getLSClient(), result);
setLSClient(newClient);
if (result) {
statusBarItem.text = `$(kedro-logo) base + ${result.label}`;
}
await sendHeapEventWithMetadata(CMD_SELECT_ENV, context);
}),
registerCommand('pygls.server.executeCommand', async () => {
await executeServerCommand(getLSClient());
}),
registerCommand(CMD_DEFINITION_REQUEST, async (word) => {
await executeServerDefinitionCommand(getLSClient(), word);
await sendHeapEventWithMetadata(CMD_DEFINITION_REQUEST, context);
}),
registerCommand(CMD_RUN_KEDRO_VIZ, async () => {
await handleKedroViz(context, getLSClient());
}),
registerCommand(CMD_SHOW_OUTPUT_CHANNEL, () => {
outputChannel.show();
}),
registerCommand(CMD_SET_PROJECT_PATH, () => {
setKedroProjectPath();
}),
);
})();
};
73 changes: 67 additions & 6 deletions src/common/commands.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,89 @@
import * as fs from 'fs';
import * as path from 'path';
import { QuickPickItem, window } from 'vscode';
import * as vscode from 'vscode';

import { getWorkspaceFolders } from './vscodeapi';
import { LanguageClient, LanguageClientOptions, ServerOptions, State, integer } from 'vscode-languageclient/node';
import { LanguageClient, State } from 'vscode-languageclient/node';
import { getKedroProjectPath, isKedroProject } from './utilities';
export async function selectEnvironment() {
let workspaces = getWorkspaceFolders();
const root_dir = workspaces[0].uri.fsPath; // Only pick the first workspace
const confDir = `${root_dir}/conf`;
let kedroProjectPath = await getKedroProjectPath();
let kedroProjectRootDir: string | undefined = undefined;

if (kedroProjectPath) {
kedroProjectRootDir = kedroProjectPath;
} else {
let workspaces = getWorkspaceFolders();
kedroProjectRootDir = workspaces[0].uri.fsPath; // Only pick the first workspace
}

const confDir = `${kedroProjectRootDir}/conf`;
// Iterate the `conf` directory to get folder names
const directories = fs
.readdirSync(confDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);

const envs: QuickPickItem[] = directories.filter(dir => dir !== 'base').map((label) => ({ label }));
const envs: QuickPickItem[] = directories.filter((dir) => dir !== 'base').map((label) => ({ label }));

const result = await window.showQuickPick(envs, {
placeHolder: 'Select Kedro runtime environment',
});

return result;
}

export async function setKedroProjectPath() {
const result = await vscode.window.showInputBox({
placeHolder: 'Enter the Kedro Project Root Directory',
prompt: 'Please provide the path to the Kedro project root directory',
validateInput: async (value) => {
if (!value) {
return 'Path cannot be empty';
}
// Verify if path exists and is a Kedro project
if (!(await isKedroProject(value))) {
return 'Invalid Kedro project path. Please ensure it contains pyproject.toml';
}
return null;
},
});

if (result) {
// Create URI from the path
const uri = vscode.Uri.file(result);

// Get current workspace folders
const currentFolders = vscode.workspace.workspaceFolders || [];

// Check if the entered path is already part of any workspace folder
const isPartOfWorkspace = currentFolders.some((folder) => {
const folderPath = folder.uri.fsPath;
return result.startsWith(folderPath) || folderPath.startsWith(result);
});

// If path is not part of workspace, add it as a new workspace folder
if (!isPartOfWorkspace) {
// Add new folder to workspace
const success = await vscode.workspace.updateWorkspaceFolders(
currentFolders.length,
0,
{ uri: uri, name: path.basename(result) }, // New folder to add
);

if (!success) {
vscode.window.showErrorMessage('Failed to add folder to workspace');
return;
}
}

// Update kedro configuration
const config = vscode.workspace.getConfiguration('kedro');
await config.update('kedroProjectPath', result, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage('Kedro project path updated successfully');
}
}

let logger: vscode.LogOutputChannel;

/**
Expand Down Expand Up @@ -113,4 +175,3 @@ export async function executeGetProjectDataCommand(lsClient: LanguageClient | un
const result = await vscode.commands.executeCommand(commandName);
return result;
}

4 changes: 3 additions & 1 deletion src/common/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ async function createServer(
environment?: string,
): Promise<LanguageClient> {
const command = settings.interpreter[0];
const cwd = settings.cwd;

// Use kedroProjectPath if set, otherwise fallback to settings.cwd
const cwd = settings.kedroProjectPath || settings.cwd;

// Set debugger path needed for debugging python code.
const newEnv = { ...process.env };
Expand Down
Loading

0 comments on commit 2523edf

Please sign in to comment.