From 887d6b9b74c8b16c62e07121c333299fc817ab85 Mon Sep 17 00:00:00 2001 From: Timur Malanin Date: Sun, 18 Jan 2026 16:34:33 +0000 Subject: [PATCH] PY-86247 Introduced an ability to choose folder that contains python binary instead of direct searching of file GitOrigin-RevId: 20ab5cb28abd08fe05c528f11583bb9f37706926 --- .../python/venvReader/VirtualEnvReader.kt | 29 ++++++++--- .../messages/PyBundle.properties | 1 + .../jetbrains/python/sdk/PythonSdkType.java | 48 ++++++++++++++++--- .../sdk/add/v2/CustomNewEnvironmentCreator.kt | 6 ++- .../jetbrains/python/sdk/add/v2/FileSystem.kt | 9 ++-- .../jetbrains/python/sdk/add/v2/uiUtils.kt | 5 +- .../python/sdk/add/v2/venv/helper.kt | 7 ++- 7 files changed, 81 insertions(+), 24 deletions(-) diff --git a/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt b/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt index 68265fae345d..55e59c143f08 100644 --- a/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt +++ b/python/openapi/src/com/jetbrains/python/venvReader/VirtualEnvReader.kt @@ -129,18 +129,33 @@ class VirtualEnvReader private constructor( /** - * [dir] is root directory of python installation or virtualenv + * [pathOrDir] is either a direct path to a Python binary or a root directory of python installation or virtualenv */ @RequiresBackgroundThread - fun findPythonInPythonRoot(dir: PythonHomePath): PythonBinary? { + fun findPythonInPythonRoot(pathOrDir: PythonHomePath): PythonBinary? { + val pythonNames = when (forcedOs ?: pathOrDir.getEelDescriptor().osFamily) { + EelOsFamily.Posix -> POSIX_BINS + EelOsFamily.Windows -> WIN_BINS + } + if (pathOrDir.isRegularFile() && pathOrDir.name.lowercase() in pythonNames) { + return pathOrDir + } - val bin = dir.resolve("bin") - findInterpreter(bin)?.let { return it } + if (!pathOrDir.isDirectory()) { + return null + } - val scripts = dir.resolve("Scripts") - findInterpreter(scripts)?.let { return it } + val bin = pathOrDir.resolve("bin") + if (bin.isDirectory()) { + findInterpreter(bin)?.let { return it } + } - return findInterpreter(dir) + val scripts = pathOrDir.resolve("Scripts") + if (scripts.isDirectory()) { + findInterpreter(scripts)?.let { return it } + } + + return findInterpreter(pathOrDir) } fun getVenvRootPath(path: Path): Path? { diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index bfbbe8f50779..6dab186b6b72 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -292,6 +292,7 @@ runcfg.labels.module.name=Module name runcfg.labels.custom.name=Custom runcfg.labels.execution=Execution python.sdk.error.invalid.interpreter.selected=An invalid Python interpreter selected ''{0}''! +python.sdk.error.invalid.venv.selected=The selected folder ''{0}'' is not a valid Python virtual environment! sdk.select.path=Select Python Interpreter runcfg.unittest.dlg.pattern=Pattern: diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkType.java b/python/src/com/jetbrains/python/sdk/PythonSdkType.java index 9f215aa70be4..3a8dd3e40e25 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkType.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkType.java @@ -42,6 +42,7 @@ import com.jetbrains.python.sdk.legacy.PythonSdkUtil; import com.jetbrains.python.target.PyDetectedSdkAdditionalData; import com.jetbrains.python.target.PyInterpreterVersionUtil; import com.jetbrains.python.target.PyTargetAwareAdditionalData; +import com.jetbrains.python.venvReader.VirtualEnvReader; import kotlin.coroutines.Continuation; import kotlin.jvm.functions.Function2; import kotlinx.coroutines.CoroutineScope; @@ -138,30 +139,63 @@ public final class PythonSdkType extends SdkType { return PythonSdkFlavor.getFlavor(path.toString()) != null; } + @ApiStatus.Internal + @Override + @RequiresBackgroundThread + public @NotNull String adjustSelectedSdkHome(@NotNull String homePath) { + try { + Path pythonPath = VirtualEnvReader.getInstance().findPythonInPythonRoot(Path.of(homePath)); + return pythonPath != null ? pythonPath.toString() : homePath; + } + catch (InvalidPathException e) { + return homePath; + } + } + @Override public @NotNull FileChooserDescriptor getHomeChooserDescriptor() { - final var descriptor = new FileChooserDescriptor(true, false, false, false, false, false) { + final var descriptor = new FileChooserDescriptor(true, true, false, false, false, false) { @Override public void validateSelectedFiles(VirtualFile @NotNull [] files) throws Exception { if (files.length != 0) { VirtualFile file = files[0]; - Boolean isValid = runWithModalProgressBlocking( + record ValidationResult(boolean isValid, boolean isDirectory) {} + + ValidationResult result = runWithModalProgressBlocking( ModalTaskOwner.guess(), PyBundle.message("modal.progress.title.path.validation"), TaskCancellation.cancellable(), new Function2<>() { @Override - public Boolean invoke(CoroutineScope scope, - Continuation continuation) { - return isLocatedInWsl(file) || isLocalPathValid(file.toNioPath()); + public ValidationResult invoke(CoroutineScope scope, + Continuation continuation) { + + try { + String adjustedPath = adjustSelectedSdkHome(file.getPath()); + boolean isValid = isLocalPathValid(Path.of(adjustedPath)); + return new ValidationResult(isLocatedInWsl(file) || isValid, file.isDirectory()); + } + catch (InvalidPathException e) { + return new ValidationResult(false, false); + } } } ); - if (!isValid) { - throw new Exception(PyBundle.message("python.sdk.error.invalid.interpreter.selected", file.getName())); + if (!result.isValid()) { + String message = result.isDirectory() + ? PyBundle.message("python.sdk.error.invalid.venv.selected", file.getName()) + : PyBundle.message("python.sdk.error.invalid.interpreter.selected", file.getName()); + throw new Exception(message); } } } + + @Override + public boolean isFileSelectable(@Nullable VirtualFile file) { + if (file == null) return false; + Path pythonPath = VirtualEnvReader.getInstance().findPythonInPythonRoot(file.toNioPath()); + return pythonPath != null; + } } .withTitle(PyBundle.message("sdk.select.path")) .withShowHiddenFiles(SystemInfo.isUnix); diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt index 5cf37e602227..9cb8b4456499 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt @@ -143,7 +143,9 @@ internal abstract class CustomNewEnvironmentCreator

( val installedSdk = when (baseInterpreter) { is InstallableSelectableInterpreter -> installBaseSdk(baseInterpreter.sdk, model.existingSdks) ?.let { - val sdkWrapper = model.fileSystem.wrapSdk(it) + val sdkWrapper = runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) { + model.fileSystem.wrapSdk(it) + } val installed = model.addInstalledInterpreter(sdkWrapper.homePath, baseInterpreter.pythonInfo) model.state.baseInterpreter.set(installed) installed @@ -181,7 +183,7 @@ internal abstract class CustomNewEnvironmentCreator

( internal open fun onVenvSelectExisting() {} } -private fun

PythonAddInterpreterModel

.installPythonIfNeeded(interpreter: PythonSelectableInterpreter

): P? { + private suspend fun

PythonAddInterpreterModel

.installPythonIfNeeded(interpreter: PythonSelectableInterpreter

): P? { // todo use target config val path = if (interpreter is InstallableSelectableInterpreter

) { installBaseSdk(interpreter.sdk, existingSdks)?.let { fileSystem.wrapSdk(it) }?.homePath diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 00c945babd55..181424e619d6 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -70,7 +70,7 @@ sealed interface FileSystem

{ suspend fun validateVenv(homePath: P): PyResult suspend fun suggestVenv(projectPath: Path): PyResult

- fun wrapSdk(sdk: Sdk): SdkWrapper

+ suspend fun wrapSdk(sdk: Sdk): SdkWrapper

suspend fun detectSelectableVenv(): List> fun preferredInterpreterBasePath(): P? = null suspend fun resolvePythonBinary(pythonHome: P): P? @@ -161,8 +161,9 @@ sealed interface FileSystem

{ return PyResult.success(interpreter) } - override fun wrapSdk(sdk: Sdk): SdkWrapper { - return SdkWrapper(sdk, PathHolder.Eel(Path.of(sdk.homePath!!))) + override suspend fun wrapSdk(sdk: Sdk): SdkWrapper = withContext(Dispatchers.IO) { + val adjustedHomePath = PythonSdkType.getInstance().adjustSelectedSdkHome(sdk.homePath!!) + SdkWrapper(sdk, PathHolder.Eel(Path.of(adjustedHomePath))) } override suspend fun detectSelectableVenv(): List> { @@ -291,7 +292,7 @@ sealed interface FileSystem

{ return PyResult.success(interpreter) } - override fun wrapSdk(sdk: Sdk): SdkWrapper { + override suspend fun wrapSdk(sdk: Sdk): SdkWrapper { return SdkWrapper(sdk, PathHolder.Target(sdk.homePath!!)) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt index 77d93f33ef0c..811cf1dc5701 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt @@ -336,8 +336,9 @@ internal class PythonInterpreterComboBox

( errorSink.emit(error) }.successOrNull - val interpreter = pathOnFileSystem?.let { - onPathSelected(it).onFailure { error -> errorSink.emit(error) }.successOrNull + val interpreter = pathOnFileSystem?.let { selectedPath -> + val pythonBinaryPath = fileSystem.resolvePythonBinary(selectedPath) ?: selectedPath + onPathSelected(pythonBinaryPath).onFailure { error -> errorSink.emit(error) }.successOrNull } interpreter?.let { interpreter -> diff --git a/python/src/com/jetbrains/python/sdk/add/v2/venv/helper.kt b/python/src/com/jetbrains/python/sdk/add/v2/venv/helper.kt index ccb121119b0c..09f486e9f6d6 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/venv/helper.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/venv/helper.kt @@ -6,7 +6,7 @@ import com.intellij.python.community.impl.venv.createVenv import com.jetbrains.python.PyBundle.message import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.errorProcessing.getOr -import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.add.v2.* suspend fun

PythonMutableTargetAddInterpreterModel

.setupVirtualenv(venvFolder: P, moduleOrProject: ModuleOrProject): PyResult { @@ -53,5 +53,8 @@ private suspend fun

PythonAddInterpreterModel

.createSdkFromB isAssociateWithModule = !venvViewModel.makeAvailableForAllProjects.get() ) - return sdkResult.mapSuccess { sdk -> fileSystem.wrapSdk(sdk) } + return when (sdkResult) { + is com.jetbrains.python.Result.Success -> PyResult.success(fileSystem.wrapSdk(sdkResult.result)) + is com.jetbrains.python.Result.Failure -> PyResult.failure(sdkResult.error) + } }