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 super Boolean> continuation) { - return isLocatedInWsl(file) || isLocalPathValid(file.toNioPath()); + public ValidationResult invoke(CoroutineScope scope, + Continuation super ValidationResult> 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
- fun wrapSdk(sdk: Sdk): SdkWrapper
+ suspend fun wrapSdk(sdk: Sdk): SdkWrapper
suspend fun detectSelectableVenv(): List {
return PyResult.success(interpreter)
}
- override fun wrapSdk(sdk: Sdk): SdkWrapper {
return PyResult.success(interpreter)
}
- override fun wrapSdk(sdk: Sdk): SdkWrapper (
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 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)
+ }
}