Skip to content

Comments

Add middleware that enhances the ty configuration section with the active Python interpreter#65

Merged
MichaReiser merged 4 commits intomainfrom
micha/python-interpreter
Jul 14, 2025
Merged

Add middleware that enhances the ty configuration section with the active Python interpreter#65
MichaReiser merged 4 commits intomainfrom
micha/python-interpreter

Conversation

@MichaReiser
Copy link
Member

@MichaReiser MichaReiser commented Jun 27, 2025

Summary

One step closer towards #26

This PR adds a custom middleware that intercepts the configuration request and it adds the information for the selected and active python interpreter. The value returned by the Python extension doesn't necessarily have to point to a valid python environment. But we can use it as a fallback if it does.

Test Plan

Screen.Recording.2025-07-10.at.14.23.50.mov

@MichaReiser MichaReiser force-pushed the micha/python-interpreter branch 2 times, most recently from 7e1ed8c to 6e934d0 Compare June 27, 2025 17:26
Comment on lines 237 to 242
if (middleware.isDidChangeConfigurationRegistered()) {
logger.debug("Python interpreter changed, sending DidChangeConfigurationNotification");

// If the Python interpreter changes,
newLSClient.sendNotification(DidChangeConfigurationNotification.type, undefined);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit overkill at the moment because we always restart the server when the interpreter changes (because we might need to use a different binary now). I kept the implementation because it took me a rather long time to figure out how to know if the server registered the capability but I can also remove it again.

I think it would also be great if we can avoid restarting the LSP if the interpreter changes....

Copy link
Member

@dhruvmanila dhruvmanila Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. I think the only reason that we would use a different binary is if the ty binary used is installed in the virtual environment where the interpreter is present.

I don't know if we can avoid the restart completely but we could check if the interpreter is even going to be used to find the binary path. If it's not going to be used, then we can avoid restarting the server when the interpreter changes.

The checks that I'm referring to is the one before line 101 in the below code:

async function findBinaryPath(settings: ISettings): Promise<string> {
if (!vscode.workspace.isTrusted) {
logger.info(`Workspace is not trusted, using bundled executable: ${BUNDLED_EXECUTABLE}`);
return BUNDLED_EXECUTABLE;
}
// 'path' setting takes priority over everything.
if (settings.path.length > 0) {
for (const path of settings.path) {
if (await fsapi.pathExists(path)) {
logger.info(`Using 'path' setting: ${path}`);
return path;
}
}
logger.info(`Could not find executable in 'path': ${settings.path.join(", ")}`);
}
if (settings.importStrategy === "useBundled") {
logger.info(`Using bundled executable: ${BUNDLED_EXECUTABLE}`);
return BUNDLED_EXECUTABLE;
}
// Otherwise, we'll call a Python script that tries to locate a binary.
let tyBinaryPath: string | undefined;
try {
const stdout = await executeFile(settings.interpreter[0], [FIND_BINARY_SCRIPT_PATH]);
tyBinaryPath = stdout.trim();
} catch (err) {
vscode.window
.showErrorMessage(
"Unexpected error while trying to find the ty binary. See the logs for more details.",
"Show Logs",
)
.then((selection) => {
if (selection) {
logger.channel.show();
}
});
logger.error(`Error while trying to find the ty binary: ${err}`);
}
if (tyBinaryPath && tyBinaryPath.length > 0) {
// First choice: the executable found by the script.
logger.info(`Using the ty binary: ${tyBinaryPath}`);
return tyBinaryPath;
}
// Second choice: the executable in the global environment.
const environmentPath = await which(BINARY_NAME, { nothrow: true });
if (environmentPath) {
logger.info(`Using environment executable: ${environmentPath}`);
return environmentPath;
}
// Third choice: bundled executable.
logger.info(`Falling back to bundled executable: ${BUNDLED_EXECUTABLE}`);
return BUNDLED_EXECUTABLE;
}

So, if we know how we're going to choose the ty binary, then we can use that information to avoid restarting the server when it's not required.

@@ -1,4 +1,4 @@
import * as util from "util";
import * as util from "node:util";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed a few issues that biome found along the way

@MichaReiser MichaReiser marked this pull request as ready for review June 27, 2025 17:30
src/client.ts Outdated
if (param.section === "ty") {
const scopeUri = param.scopeUri ? Uri.parse(param.scopeUri) : undefined;
const activeEnvironment =
pythonExtension.environments.getActiveEnvironmentPath(scopeUri);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure yet if we should call resolveEnvironment here. The benefit is that we only get valid environments. The downside is that ty has to repeat this work and may still decide that this isn't an active environment (for some reason or another)

@dhruvmanila dhruvmanila self-requested a review June 30, 2025 06:53
@dhruvmanila dhruvmanila self-assigned this Jun 30, 2025
Comment on lines -15 to +18
async function getPythonExtensionAPI(): Promise<PythonExtension | undefined> {
if (_api) {
return _api;
}
_api = await PythonExtension.api();
return _api;
export async function getPythonExtensionAPI(): Promise<PythonExtension> {
const api = _api || (await PythonExtension.api());
_api = api;
return api;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to your PR, we might have to make this optional again after #5

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need to make this optional as part of #5

Comment on lines 237 to 242
if (middleware.isDidChangeConfigurationRegistered()) {
logger.debug("Python interpreter changed, sending DidChangeConfigurationNotification");

// If the Python interpreter changes,
newLSClient.sendNotification(DidChangeConfigurationNotification.type, undefined);
}
Copy link
Member

@dhruvmanila dhruvmanila Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. I think the only reason that we would use a different binary is if the ty binary used is installed in the virtual environment where the interpreter is present.

I don't know if we can avoid the restart completely but we could check if the interpreter is even going to be used to find the binary path. If it's not going to be used, then we can avoid restarting the server when the interpreter changes.

The checks that I'm referring to is the one before line 101 in the below code:

async function findBinaryPath(settings: ISettings): Promise<string> {
if (!vscode.workspace.isTrusted) {
logger.info(`Workspace is not trusted, using bundled executable: ${BUNDLED_EXECUTABLE}`);
return BUNDLED_EXECUTABLE;
}
// 'path' setting takes priority over everything.
if (settings.path.length > 0) {
for (const path of settings.path) {
if (await fsapi.pathExists(path)) {
logger.info(`Using 'path' setting: ${path}`);
return path;
}
}
logger.info(`Could not find executable in 'path': ${settings.path.join(", ")}`);
}
if (settings.importStrategy === "useBundled") {
logger.info(`Using bundled executable: ${BUNDLED_EXECUTABLE}`);
return BUNDLED_EXECUTABLE;
}
// Otherwise, we'll call a Python script that tries to locate a binary.
let tyBinaryPath: string | undefined;
try {
const stdout = await executeFile(settings.interpreter[0], [FIND_BINARY_SCRIPT_PATH]);
tyBinaryPath = stdout.trim();
} catch (err) {
vscode.window
.showErrorMessage(
"Unexpected error while trying to find the ty binary. See the logs for more details.",
"Show Logs",
)
.then((selection) => {
if (selection) {
logger.channel.show();
}
});
logger.error(`Error while trying to find the ty binary: ${err}`);
}
if (tyBinaryPath && tyBinaryPath.length > 0) {
// First choice: the executable found by the script.
logger.info(`Using the ty binary: ${tyBinaryPath}`);
return tyBinaryPath;
}
// Second choice: the executable in the global environment.
const environmentPath = await which(BINARY_NAME, { nothrow: true });
if (environmentPath) {
logger.info(`Using environment executable: ${environmentPath}`);
return environmentPath;
}
// Third choice: bundled executable.
logger.info(`Falling back to bundled executable: ${BUNDLED_EXECUTABLE}`);
return BUNDLED_EXECUTABLE;
}

So, if we know how we're going to choose the ty binary, then we can use that information to avoid restarting the server when it's not required.

@dhruvmanila
Copy link
Member

I'm assuming that this should be merged along with astral-sh/ruff#19012, right?

@dhruvmanila dhruvmanila removed their assignment Jul 7, 2025
@dhruvmanila
Copy link
Member

So, all in all, to fully support this, we require implementing the following in the server:

I added a dbg statement in the ty server's configuration response handler and the pythonExtension.activeEnvironment setting was set correctly.

I might be missing something but how does this work then without registering the didChangeConfiguration notification?

Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I think we need to validate and probably change the way registration is being handled here. We probably don't need to look at the registration options for the didChangeConfiguration notification (as it doesn't exists) and directly add the id to didChangeRegistrations. I think the TyMiddleware is only for the ty server so that should be accurate and the server won't be providing any registration options either.

@MichaReiser MichaReiser force-pushed the micha/python-interpreter branch from 6a606ee to 0995df8 Compare July 14, 2025 09:52
@MichaReiser MichaReiser merged commit 78c1803 into main Jul 14, 2025
1 check passed
@MichaReiser MichaReiser deleted the micha/python-interpreter branch July 14, 2025 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants