Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support settings of custom project path in Kedro Extension #162

Merged
merged 5 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "Kedro: Set Project Path",
rashidakanchwala marked this conversation as resolved.
Show resolved Hide resolved
"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
Loading