Various validation refactorings

`readableFs` is redundant and unstable. Remove it: we will migrate to ijent anyway.
All validations are in `PathValidator.kt` now. They are used by `ManualPathEntryDialog` and sdk validation.
Lots of thread annotations added to prevent calling validation code from EDT.

In general, this change makes path validation ready for ijent: validation based on nio with slow IO access.

Validation is removed from old, non-target classes

(cherry picked from commit 185b4f7fe8cbd5d7a37dad609c8a4cb8163d6eed)

IJ-MR-112281

GitOrigin-RevId: 12c4a4f3d459d0523ef6694a9e4bb2db7a1582b7
This commit is contained in:
Ilya.Kazakevich
2023-07-29 21:53:36 +02:00
committed by intellij-monorepo-bot
parent 40ffdcfa14
commit 2bb2ea60fc
41 changed files with 388 additions and 413 deletions

View File

@@ -2,24 +2,17 @@
package com.intellij.execution.wsl.target
import com.intellij.execution.target.*
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs
import com.intellij.execution.wsl.WSLDistribution
import com.intellij.execution.wsl.WslDistributionManager
import com.intellij.execution.wsl.listWindowsLocalDriveRoots
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.SystemInfoRt
import com.sun.jna.platform.win32.Kernel32.*
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.pathString
class WslTargetEnvironmentConfiguration() : TargetConfigurationWithId(WslTargetType.TYPE_ID),
PersistentStateComponent<WslTargetEnvironmentConfiguration.MyState>,
PersistentTargetEnvironmentConfiguration,
TargetConfigurationReadableFs,
TargetConfigurationWithLocalFsAccess {
override val asTargetConfig: TargetEnvironmentConfiguration = this
@@ -64,25 +57,6 @@ class WslTargetEnvironmentConfiguration() : TargetConfigurationWithId(WslTargetT
return "WslTargetEnvironmentConfiguration(distributionId=$distributionIdText, projectRootOnTarget='$projectRootOnTarget')"
}
override fun getPathInfo(targetPath: String): PathInfo? {
// TODO: 9P is unreliable and we must migrate to some tool running in WSL (like ijent)
assert(SystemInfoRt.isWindows) { "WSL is for Windows only" }
val distribution = distribution
if (distribution == null) {
thisLogger().warn("No distribution, cant check path")
return null
}
val winLocalPath = Paths.get(distribution.getWindowsPath(targetPath))
val fileAttributes = INSTANCE.GetFileAttributes(winLocalPath.pathString)
// Reparse point is probably symlink, but could be dir or file. See https://github.com/microsoft/WSL/issues/5118
if (fileAttributes != INVALID_FILE_ATTRIBUTES && fileAttributes.and(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT) {
return PathInfo.Unknown
}
val pathInfo = PathInfo.getPathInfoForLocalPath(winLocalPath)
// We can't check if file is executable or not (we could, but it is too heavy), so we set this flag
return if (pathInfo is PathInfo.RegularFile) pathInfo.copy(executable = true) else pathInfo
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@@ -1,36 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.execution.target.readableFs
import java.io.File
import java.nio.file.Path
import kotlin.io.path.*
/**
* Abstraction over target path because target paths (like ssh or wsl) can't always be represented as [Path].
*/
sealed class PathInfo {
/**
* File system object exists, but we do not know what is it
*/
object Unknown: PathInfo()
data class Directory(val empty: Boolean) : PathInfo()
data class RegularFile(val executable: Boolean) : PathInfo()
companion object {
val localPathInfoProvider: TargetConfigurationReadableFs = TargetConfigurationReadableFs { getPathInfoForLocalPath(Path.of(it)) }
fun getPathInfoForLocalPath(localPath: Path): PathInfo? =
when {
(!localPath.exists()) -> tryGetUsingOldApi(localPath.toFile())
localPath.isRegularFile() -> RegularFile(localPath.isExecutable())
localPath.isDirectory() -> Directory(localPath.listDirectoryEntries().isEmpty())
else -> null
}
private fun tryGetUsingOldApi(file: File): PathInfo? = when {
(!file.exists())-> null
file.isFile -> RegularFile(file.canExecute())
file.isDirectory -> Directory(file.list().isEmpty())
else -> null
}
}
}

View File

@@ -1,15 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.execution.target.readableFs
/**
* This target configuration provides access to filesystem, so we can check if certain path is file, directory etc
*/
@FunctionalInterface
fun interface TargetConfigurationReadableFs {
/**
* Checks [targetPath] against target file system. `null` means file not found.
*/
fun getPathInfo(targetPath: String): PathInfo?
}

View File

@@ -79,4 +79,11 @@ public final class ValidationInfo {
this.okEnabled == that.okEnabled &&
this.warning == that.warning;
}
@Override
public String toString() {
return "ValidationInfo{" +
"message='" + message + '\'' +
'}';
}
}

View File

@@ -1,39 +0,0 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.execution.wsl
import com.intellij.execution.target.TargetConfigurationWithLocalFsAccess
import com.intellij.execution.target.readableFs.PathInfo.*
import com.intellij.execution.wsl.target.WslTargetEnvironmentConfiguration
import com.intellij.testFramework.fixtures.TestFixtureRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.assertEquals
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
/**
* Test [TargetConfigurationWithLocalFsAccess] for WSL
*/
class WslPathInfoTest {
companion object {
private val appRule = TestFixtureRule()
private val wslRule = WslRule()
private val wslTempDirRule = WslTempDirRule(wslRule)
@ClassRule
@JvmField
val ruleChain: RuleChain = RuleChain.outerRule(appRule).around(wslRule).around(wslTempDirRule)
}
@Test
fun testPathInfo() {
val target = WslTargetEnvironmentConfiguration(wslRule.wsl)
assertThat(target.getPathInfo("/path/doesn/exist")).isNull()
assertThat(target.getPathInfo("/bin")).isIn(Unknown, Directory(false))
assertThat(target.getPathInfo("/usr/bin")).isIn(Unknown, Directory(false))
assertThat(target.getPathInfo("/bin/ls") ?: target.getPathInfo("/usr/bin/ls")).isIn(Unknown, RegularFile(true))
assertThat(target.getPathInfo("/etc/resolv.conf")).isIn(Unknown, RegularFile(false))
val emptyDir = wslTempDirRule.linuxPath
assertEquals("Empty dir", Directory(true), target.getPathInfo(emptyDir))
}
}

View File

@@ -3,7 +3,6 @@ package com.jetbrains.python.sdk.configuration
import com.intellij.codeInspection.util.IntentionName
import com.intellij.execution.ExecutionException
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
@@ -19,10 +18,12 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBLabel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyCharmCommunityCustomizationBundle
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.psi.PyUtil
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.PythonSdkUpdater
@@ -66,6 +67,7 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
private fun getEnvironmentYml(module: Module) = PyUtil.findInRoots(module, "environment.yml")
@RequiresBackgroundThread
private fun createAndAddSdk(module: Module, source: Source): Sdk? {
val targetConfig = PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(module)
if (targetConfig != null) {
@@ -79,12 +81,13 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
}
}
@RequiresBackgroundThread
private fun askForEnvData(module: Module, source: Source): PyAddNewCondaEnvFromFilePanel.Data? {
val environmentYml = getEnvironmentYml(module) ?: return null
// Again: only local conda is supported for now
val condaExecutable = runBlocking { suggestCondaPath() }?.let { LocalFileSystem.getInstance().findFileByPath(it) }
if (source == Source.INSPECTION && CondaEnvSdkFlavor.validateCondaPath(condaExecutable?.path, PathInfo.localPathInfoProvider) == null) {
if (source == Source.INSPECTION && CondaEnvSdkFlavor.validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
PySdkConfigurationCollector.logCondaEnvDialogSkipped(module.project, source, executableToEventField(condaExecutable?.path))
return PyAddNewCondaEnvFromFilePanel.Data(condaExecutable!!.path, environmentYml.path)
}

View File

@@ -27,8 +27,10 @@ import com.jetbrains.python.sdk.configuration.PySdkConfigurationCollector.Compan
import com.jetbrains.python.sdk.pipenv.*
import java.awt.BorderLayout
import java.awt.Insets
import java.nio.file.Path
import javax.swing.JComponent
import javax.swing.JPanel
import kotlin.io.path.isExecutable
class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
@@ -50,8 +52,8 @@ class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
private fun askForEnvData(module: Module, source: Source): PyAddNewPipEnvFromFilePanel.Data? {
val pipEnvExecutable = getPipEnvExecutable()?.absolutePath
if (source == Source.INSPECTION && validatePipEnvExecutable(pipEnvExecutable) == null) {
return PyAddNewPipEnvFromFilePanel.Data(pipEnvExecutable!!)
if (source == Source.INSPECTION && pipEnvExecutable?.let { Path.of(it).isExecutable() } == true) {
return PyAddNewPipEnvFromFilePanel.Data(pipEnvExecutable)
}
var permitted = false

View File

@@ -381,8 +381,6 @@ python.sdk.poetry.environment.panel.title=Poetry Environment
python.sdk.poetry.install.packages.from.toml.checkbox.text=Install packages from pyproject.toml
python.sdk.poetry.dialog.message.poetry.interpreter.has.been.already.added=Poetry interpreter has been already added, select ''{0}''
python.sdk.file.not.found=File {0} is not found
python.sdk.cannot.execute=Cannot execute {0}
python.sdk.pipenv.has.been.selected=Pipenv interpreter has been already added, select ''{0}'' in your interpreters list
python.sdk.there.is.no.interpreter=No interpreter
python.sdk.no.interpreter.configured.warning=No Python interpreter configured for the project
@@ -1354,8 +1352,6 @@ python.template.language.none=None
enter.path.dialog.title=Enter Path
path.label=Path:
path.must.not.be.empty.error.message=Path must not be empty
path.must.be.absolute.error.message=Path must be absolute
path.ends.with.whitespace.warning.message=Path ends with a whitespace
# Python run target language
python.language.configure.label=Python Configuration
@@ -1455,3 +1451,4 @@ inlay.parameters.python.hints.blacklist.explanation=\
<p>Names or placeholders must be provided for all parameters, including the optional ones.<br>\
Qualified method names must include class names, or placeholders for them.<br>\
Use the "Do not show hints for current method" {0} action to add patterns from the editor.</p>

View File

@@ -28,5 +28,6 @@
<orderEntry type="library" name="commons-collections" level="project" />
<orderEntry type="library" name="jna" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" name="caffeine" level="project" />
</component>
</module>

View File

@@ -68,4 +68,14 @@ python.configure.interpreter.action=Configure a Python interpreter\u2026
python.configuring.interpreter.progress=Configuring a Python interpreter
# Skeletons generator
dialog.message.broken.home.path.for=Broken home path for {0}
dialog.message.broken.home.path.for=Broken home path for {0}
path.validation.wait=Validating path, Please Wait...
path.validation.wait.path=Validating {0}, Please Wait...
path.validation.field.empty=Path field is empty
path.validation.cannot.execute=Cannot execute {0}
path.validation.must.be.absolute=Path must be absolute
path.validation.ends.with.whitespace=Path ends with a whitespace
path.validation.file.not.found=File {0} is not found
path.validation.invalid=Path is invalid: {0}
path.validation.inaccessible=Path is inaccessible

View File

@@ -0,0 +1,87 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.pathValidation
import com.intellij.execution.Platform
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.io.OSAgnosticPathUtil
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.io.isDirectory
import com.intellij.util.io.isFile
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.sdk.appxProduct
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import java.io.IOException
import java.nio.file.InvalidPathException
import java.nio.file.Path
import javax.swing.JComponent
import kotlin.io.path.isExecutable
import kotlin.io.path.listDirectoryEntries
/**
* To be used with [validateExecutableFile] and [validateEmptyDir]
* @param path from path field value.
* @param fieldIsEmpty message to show if field is empty
* @param component see [ValidationInfo.component]
* @param platformAndRoot to validate path against
*/
class ValidationRequest(@NonNls internal val path: String?,
@Nls val fieldIsEmpty: String = PySdkBundle.message("path.validation.field.empty"),
private val platformAndRoot: PlatformAndRoot,
private val component: JComponent? = null) {
internal fun validate(getMessage: (Path) -> @Nls String?): ValidationInfo? {
val message: @Nls String? = when {
path.isNullOrEmpty() -> fieldIsEmpty
!isAbsolutePath(path) -> PySdkBundle.message("path.validation.must.be.absolute")
path.endsWith(" ") -> PySdkBundle.message("path.validation.ends.with.whitespace")
else -> platformAndRoot.root?.let {
try {
val nioPath = it.resolve(path)
getMessage(nioPath)
}
catch (e: InvalidPathException) {
PySdkBundle.message("path.validation.invalid", e.message)
}
catch (e: IOException) {
PySdkBundle.message("path.validation.inaccessible", e.message)
}
}
}
return message?.let { ValidationInfo(it, component) }
}
private fun isAbsolutePath(path: String): Boolean = when (platformAndRoot.platform) {
Platform.UNIX -> path.startsWith("/")
Platform.WINDOWS -> OSAgnosticPathUtil.isAbsoluteDosPath(path)
}
}
/**
* Ensure file is executable
*/
@RequiresBackgroundThread
fun validateExecutableFile(
request: ValidationRequest
): ValidationInfo? = request.validate {
if (it.appxProduct != null) return@validate null // Nio can't be used to validate appx, assume file is valid
when {
it.isFile() -> if (it.isExecutable()) null else PySdkBundle.message("path.validation.cannot.execute", it)
it.isDirectory() -> PySdkBundle.message("path.validation.cannot.execute", it)
else -> PySdkBundle.message("path.validation.file.not.found", it)
}
}
/**
* Ensure directory either doesn't exist or empty
*/
@RequiresBackgroundThread
fun validateEmptyDir(request: ValidationRequest,
@Nls notADirectory: String,
@Nls directoryNotEmpty: String
): ValidationInfo? = request.validate {
when {
it.isDirectory() -> if (it.listDirectoryEntries().isEmpty()) null else directoryNotEmpty
it.isFile() || it.appxProduct != null -> notADirectory
else -> null
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.pathValidation
import com.intellij.execution.Platform
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import java.nio.file.Path
/**
* Against what path must be validated. [root] is now nullable since no targets provide nio access, but that will be fixed soon.
*/
class PlatformAndRoot private constructor(val root: Path?, val platform: Platform) {
companion object {
/**
* Local system
*/
val local: PlatformAndRoot = PlatformAndRoot(Path.of(""), Platform.current())
/**
* Creates [PlatformAndRoot] for [TargetEnvironmentConfiguration]. If null then returns either [local] or [platform] only depending
* on [defaultIsLocal]
*/
@RequiresBackgroundThread
fun TargetEnvironmentConfiguration?.getPlatformAndRoot(defaultIsLocal: Boolean = true): PlatformAndRoot {
val unknownTarget = PlatformAndRoot(null, Platform.UNIX)
return when {
this == null -> if (defaultIsLocal) local else unknownTarget
else -> unknownTarget //Non-null target is never local
}
}
}
}

View File

@@ -0,0 +1,16 @@
package com.jetbrains.python.sdk.flavors
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
/**
* Checks if file is executable. If no -- returns error
*/
@RequiresBackgroundThread
internal fun getFileExecutionError(@NonNls fullPath: String, targetEnvConfig: TargetEnvironmentConfiguration?): @Nls String? =
validateExecutableFile(ValidationRequest(fullPath, platformAndRoot = targetEnvConfig.getPlatformAndRoot()))?.message

View File

@@ -1,15 +1,18 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.flavors;
import com.google.common.collect.EvictingQueue;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.execution.target.TargetConfigurationWithId;
import com.intellij.execution.target.TargetEnvironmentConfiguration;
import com.intellij.execution.target.readableFs.PathInfo;
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.util.UserDataHolder;
@@ -18,6 +21,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PatternUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PySdkBundle;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.run.CommandLinePatcher;
import com.jetbrains.python.sdk.PyRemoteSdkAdditionalDataMarker;
@@ -25,16 +29,19 @@ import com.jetbrains.python.sdk.PySdkUtil;
import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.PythonSdkAdditionalData;
import icons.PythonSdkIcons;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import static com.jetbrains.python.sdk.flavors.PySdkFlavorUtilKt.getFileExecutionError;
/**
* Flavor is a type of python interpreter stored in {@link PythonSdkAdditionalData}.
@@ -45,10 +52,13 @@ import java.util.regex.Pattern;
public abstract class PythonSdkFlavor<D extends PyFlavorData> {
public static final ExtensionPointName<PythonSdkFlavor<?>> EP_NAME = ExtensionPointName.create("Pythonid.pythonSdkFlavor");
/**
* To prevent log pollution, we cache every {@link #isFileExecutable(String, TargetEnvironmentConfiguration)} call
* To prevent log pollution and slowness, we cache every {@link #isFileExecutable(String, TargetEnvironmentConfiguration)} call
* and only log it once
*/
private static final Collection<String> ourBuffer = Collections.synchronizedCollection(EvictingQueue.create(10));
private static final Cache<@NotNull String, @NotNull Boolean> ourExecutableFiles = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
private static final Pattern VERSION_RE = Pattern.compile("(Python \\S+).*");
private static final Logger LOG = Logger.getInstance(PythonSdkFlavor.class);
@@ -153,30 +163,58 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
* @param fullPath full path on target
*/
protected static boolean isFileExecutable(@NotNull String fullPath, @Nullable TargetEnvironmentConfiguration targetEnvConfig) {
boolean executable = isFileExecutableImpl(fullPath, targetEnvConfig);
if (!executable) {
if (!ourBuffer.contains(fullPath)) {
ourBuffer.add(fullPath);
Logger.getInstance(PythonSdkFlavor.class).warn(String.format("%s is not executable", fullPath));
}
var id = getIdForCache(fullPath, targetEnvConfig);
Boolean executable = ourExecutableFiles.getIfPresent(id);
if (executable != null) {
return executable;
}
return executable;
var error = getErrorIfNotExecutable(fullPath, targetEnvConfig);
if (error != null) {
Logger.getInstance(PythonSdkFlavor.class).warn(String.format("%s is not executable: %s", fullPath, error));
}
var newValue = error == null;
ourExecutableFiles.put(id, newValue);
return newValue;
}
private static boolean isFileExecutableImpl(@NotNull String fullPath, @Nullable TargetEnvironmentConfiguration targetEnvConfig) {
if (targetEnvConfig == null) {
// Local
return Files.isExecutable(Path.of(fullPath));
@Nullable
@Nls
private static String getErrorIfNotExecutable(@NotNull String fullPath, @Nullable TargetEnvironmentConfiguration targetEnvConfig) {
if (SwingUtilities.isEventDispatchThread()) {
// Run under progress
// TODO: use pyModalBlocking when we merge two modules
return ProgressManager.getInstance()
.run(new Task.WithResult<@Nullable @Nls String, RuntimeException>(null, PySdkBundle.message("path.validation.wait.path", fullPath),
false) {
@Override
@Nls
@Nullable
protected String compute(@NotNull ProgressIndicator indicator) throws RuntimeException {
return getFileExecutionError(fullPath, targetEnvConfig);
}
});
}
if (targetEnvConfig instanceof TargetConfigurationReadableFs) {
var fileInfo = ((TargetConfigurationReadableFs)targetEnvConfig).getPathInfo(fullPath);
if (fileInfo instanceof PathInfo.Unknown) {
return true; // We can't be sure if file is executable or not
}
return (fileInfo instanceof PathInfo.RegularFile) && (((PathInfo.RegularFile)fileInfo).getExecutable());
else {
return getFileExecutionError(fullPath, targetEnvConfig);
}
// We can't be sure if file is executable or not
return true;
}
@NotNull
private static String getIdForCache(@NotNull String fullPath, @Nullable TargetEnvironmentConfiguration configuration) {
var builder = new StringBuilder(fullPath);
builder.append(" ");
if (configuration instanceof TargetConfigurationWithId) {
var typeAndTargetId = ((TargetConfigurationWithId)configuration).getTargetAndTypeId();
builder.append(typeAndTargetId.component1().toString());
builder.append(typeAndTargetId.getSecond());
}
else if (configuration != null) {
builder.append(configuration.getClass().getName());
}
else {
builder.append("local");
}
return builder.toString();
}
public static @NotNull List<PythonSdkFlavor<?>> getApplicableFlavors() {

View File

@@ -2,7 +2,6 @@
package com.jetbrains.python.black
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ValidationInfo
@@ -13,8 +12,9 @@ import com.jetbrains.python.PythonFileType
import com.jetbrains.python.packaging.PyPackage
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.pyi.PyiFileType
import com.jetbrains.python.sdk.add.target.ValidationRequest
import com.jetbrains.python.sdk.add.target.validateExecutableFile
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import org.jetbrains.annotations.SystemDependent
import java.io.File
@@ -56,7 +56,7 @@ class BlackFormatterUtil {
return validateExecutableFile(ValidationRequest(
path = path,
fieldIsEmpty = PyBundle.message("black.executable.not.found", if (SystemInfo.isWindows) 0 else 1),
pathInfoProvider = PathInfo.localPathInfoProvider
platformAndRoot = PlatformAndRoot.local
))
}
}

View File

@@ -118,7 +118,7 @@ class PyRemotePathEditor extends PythonPathEditor {
return remoteInterpreterManager.chooseRemoteFiles(myProject, (PyRemoteSdkAdditionalDataBase)sdkAdditionalData, false);
}
else if (sdkAdditionalData instanceof PyTargetAwareAdditionalData) {
var dialog = new ManualPathEntryDialog(myProject, Platform.UNIX,
var dialog = new ManualPathEntryDialog(myProject,
((PyTargetAwareAdditionalData)sdkAdditionalData).getTargetEnvironmentConfiguration());
if (dialog.showAndGet()) {
return new String[]{dialog.getPath()};

View File

@@ -457,8 +457,7 @@ fun Sdk.configureBuilderToRunPythonOnTarget(targetCommandLineBuilder: TargetedCo
* The actual check logic is located in [PythonSdkFlavor.sdkSeemsValid] and its overrides. In general, the method check whether the path to
* the Python binary stored in this [Sdk] exists and the corresponding file can be executed. This check can be performed both locally and
* on a target. The latter case takes place when [PythonSdkAdditionalData] of this [Sdk] implements [PyTargetAwareAdditionalData] and the
* corresponding target provides file system operations (by implementing the interface
* [com.intellij.execution.target.readableFs.TargetConfigurationReadableFs]).
* corresponding target provides file system operations (see [com.jetbrains.python.pathValidation.ValidationRequest]).
*
* Note that if [PythonSdkAdditionalData] of this [Sdk] is [PyRemoteSdkAdditionalData] this method does not do any checks and returns
* `true`. This behavior may be improved in the future by generating [TargetEnvironmentConfiguration] based on the present

View File

@@ -85,7 +85,7 @@ open class PyAddExistingCondaEnvPanel(private val project: Project?,
}
override fun validateAll(): List<ValidationInfo> {
return listOfNotNull(validateSdkComboBox(sdkComboBox, this), CondaEnvSdkFlavor.validateCondaPath(condaPathField.text))
return listOfNotNull(validateSdkComboBox(sdkComboBox, this))
}
override fun getOrCreateSdk(): Sdk? {

View File

@@ -66,8 +66,7 @@ class PyAddNewCondaEnvFromFilePanel(private val module: Module, localCondaBinary
initialEnvironmentYmlPath = environmentYmlField.text
}
fun validateAll(): List<ValidationInfo> = listOfNotNull(
CondaEnvSdkFlavor.validateCondaPath(condaPathField.text))
fun validateAll(): List<ValidationInfo> = emptyList() // No validation for pre-target
/**
* Must be called if the input is confirmed and the current instance will not be used anymore

View File

@@ -2,7 +2,6 @@
package com.jetbrains.python.sdk.add
import com.intellij.execution.ExecutionException
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProgressIndicator
@@ -26,7 +25,6 @@ import com.jetbrains.python.packaging.PyCondaPackageService
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.target.conda.condaSupportedLanguages
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import icons.PythonIcons
@@ -106,7 +104,7 @@ open class PyAddNewCondaEnvPanel(
}
override fun validateAll(): List<ValidationInfo> =
listOfNotNull(CondaEnvSdkFlavor.validateCondaPath(condaPathField.text), validateEnvironmentDirectoryLocation(pathField, PathInfo.localPathInfoProvider))
emptyList() // Pre target validation is not supported
override fun getOrCreateSdk(): Sdk? {
val condaPath = condaPathField.text

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.add
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.module.Module
@@ -20,6 +19,7 @@ import com.jetbrains.python.PythonFileType
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.add.PyAddNewEnvCollector.Companion.InputData
import com.jetbrains.python.sdk.add.PyAddNewEnvCollector.Companion.RequirementsTxtOrSetupPyData
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.sdk.basePath
import org.jetbrains.annotations.SystemDependent
import org.jetbrains.annotations.SystemIndependent
@@ -84,7 +84,7 @@ class PyAddNewVirtualEnvFromFilePanel(private val module: Module,
}
fun validateAll(@NlsContexts.Button defaultButtonName: String): List<ValidationInfo> =
listOfNotNull(PyAddSdkPanel.validateEnvironmentDirectoryLocation(pathField, PathInfo.localPathInfoProvider),
listOfNotNull(PyAddSdkPanel.validateEnvironmentDirectoryLocation(pathField, PlatformAndRoot.local),
PyAddSdkPanel.validateSdkComboBox(baseSdkField, defaultButtonName))
/**

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.add
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
@@ -17,6 +16,7 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration
import com.jetbrains.python.statistics.InterpreterTarget
@@ -71,7 +71,7 @@ open class PyAddNewVirtualEnvPanel(private val project: Project?,
}
override fun validateAll(): List<ValidationInfo> =
listOfNotNull(validateEnvironmentDirectoryLocation(pathField, PathInfo.localPathInfoProvider),
listOfNotNull(validateEnvironmentDirectoryLocation(pathField, PlatformAndRoot.local),
validateSdkComboBox(baseSdkField, this))
override fun getOrCreateSdk(): Sdk? {

View File

@@ -16,7 +16,6 @@
package com.jetbrains.python.sdk.add
import com.intellij.CommonBundle
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs
import com.intellij.ide.IdeBundle
import com.intellij.openapi.application.AppUIExecutor
import com.intellij.openapi.application.ApplicationManager
@@ -28,15 +27,19 @@ import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.UserDataHolder
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProject.steps.PyAddNewEnvironmentPanel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.PyAddSdkDialogFlowAction.OK
import com.jetbrains.python.sdk.add.target.ValidationRequest
import com.jetbrains.python.sdk.add.target.validateEmptyDir
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateEmptyDir
import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration
import com.jetbrains.python.sdk.flavors.MacPythonSdkFlavor
import com.jetbrains.python.ui.pyModalBlocking
import icons.PythonIcons
import java.awt.Component
import javax.swing.Icon
@@ -80,16 +83,22 @@ abstract class PyAddSdkPanel : JPanel(), PyAddSdkView {
companion object {
@JvmStatic
fun validateEnvironmentDirectoryLocation(field: TextFieldWithBrowseButton, pathInfoProvider: TargetConfigurationReadableFs? = null): ValidationInfo? =
validateEmptyDir(
ValidationRequest(
path = field.text,
fieldIsEmpty = PySdkBundle.message("python.venv.location.field.empty"),
pathInfoProvider = pathInfoProvider
),
notADirectory = PySdkBundle.message("python.venv.location.field.not.directory"),
directoryNotEmpty = PySdkBundle.message("python.venv.location.directory.not.empty")
)
@RequiresEdt
@RequiresBlockingContext
fun validateEnvironmentDirectoryLocation(field: TextFieldWithBrowseButton, platformAndRoot: PlatformAndRoot): ValidationInfo? {
val path = field.text
return pyModalBlocking {
validateEmptyDir(
ValidationRequest(
path = path,
fieldIsEmpty = PySdkBundle.message("python.venv.location.field.empty"),
platformAndRoot = platformAndRoot
),
notADirectory = PySdkBundle.message("python.venv.location.field.not.directory"),
directoryNotEmpty = PySdkBundle.message("python.venv.location.directory.not.empty")
)
}
}
/** Should be protected. Please, don't use outside the class. KT-48508 */
@JvmStatic

View File

@@ -3,6 +3,7 @@ package com.jetbrains.python.sdk.add
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.annotations.Nls
import java.awt.Component
import javax.swing.Icon
@@ -74,6 +75,7 @@ interface PyAddSdkView {
*
* @see com.intellij.openapi.ui.DialogWrapper.doValidateAll
*/
@RequiresEdt
fun validateAll(): List<ValidationInfo>
fun addStateListener(stateListener: PyAddSdkStateListener)

View File

@@ -96,7 +96,7 @@ class PySdkPathChoosingComboBox @JvmOverloads constructor(sdks: List<Sdk> = empt
else {
// The fallback where the path is entered manually
ActionListener {
val dialog = ManualPathEntryDialog(project = null, platform = Platform.UNIX)
val dialog = ManualPathEntryDialog(project = null, targetEnvironmentConfiguration)
if (dialog.showAndGet()) {
childComponent.selectedItem = createDetectedSdk(dialog.path, targetEnvironmentConfiguration).apply { addSdkItemOnTop(this) }
}

View File

@@ -1,55 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.add.target
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs
import com.intellij.openapi.ui.ValidationInfo
import com.jetbrains.python.PyBundle
import org.jetbrains.annotations.Nls
import javax.swing.JComponent
/**
* To be used with [validateExecutableFile] and [validateEmptyDir]
* [path] is target path. [fieldIsEmpty] is an error message, [pathInfoProvider] is from target (validation skipped if null, only emptiness checked)
*/
class ValidationRequest(internal val path: String?,
@Nls val fieldIsEmpty: String,
private val pathInfoProvider: TargetConfigurationReadableFs? = null,
private val component: JComponent? = null) {
internal fun validate(getMessage: (PathInfo?) -> @Nls String?): ValidationInfo? {
val message: @Nls String? = when {
path.isNullOrBlank() -> fieldIsEmpty
else -> pathInfoProvider?.let { getMessage(it.getPathInfo(path)) }
}
return message?.let { ValidationInfo(it, component) }
}
}
/**
* Ensure file is executable
*/
fun validateExecutableFile(
request: ValidationRequest
): ValidationInfo? = request.validate {
when (it) {
is PathInfo.Unknown -> null
is PathInfo.RegularFile -> if (it.executable) null else PyBundle.message("python.sdk.cannot.execute", request.path)
is PathInfo.Directory -> PyBundle.message("python.sdk.cannot.execute", request.path)
else -> PyBundle.message("python.sdk.file.not.found", request.path)
}
}
/**
* Ensure directory either doesn't exist or empty
*/
fun validateEmptyDir(request: ValidationRequest,
@Nls notADirectory: String,
@Nls directoryNotEmpty: String
): ValidationInfo? = request.validate {
when (it) {
is PathInfo.Unknown -> null
is PathInfo.Directory -> if (it.empty) null else directoryNotEmpty
is PathInfo.RegularFile -> notADirectory
else -> null
}
}

View File

@@ -2,7 +2,6 @@
package com.jetbrains.python.sdk.add.target
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
@@ -38,12 +37,6 @@ abstract class PyAddSdkPanelBase(protected val project: Project?,
protected val targetEnvironmentConfiguration: TargetEnvironmentConfiguration?
get() = targetSupplier?.get()
/**
* For targets providing access to FS returns instance to map target path to abstraction used by validation.
* Otherwise return null, so [validateExecutableFile] and [validateEmptyDir] skips validations
*/
protected val pathInfoProvider: TargetConfigurationReadableFs? = targetEnvironmentConfiguration as? TargetConfigurationReadableFs
protected val isUnderLocalTarget: Boolean
get() = targetEnvironmentConfiguration == null

View File

@@ -4,7 +4,6 @@ package com.jetbrains.python.sdk.add.target
import com.intellij.execution.ExecutionException
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.joinTargetPaths
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProgressIndicator
@@ -33,6 +32,7 @@ import com.jetbrains.python.sdk.add.ExistingPySdkComboBoxItem
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
import com.jetbrains.python.sdk.add.addBaseInterpretersAsync
import com.jetbrains.python.sdk.add.addInterpretersAsync
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.target.PyTargetAwareAdditionalData
@@ -167,8 +167,8 @@ class PyAddVirtualEnvPanel constructor(project: Project?,
override fun validateAll(): List<ValidationInfo> {
if (newEnvironmentModeSelected()) {
val provider = pathInfoProvider ?: if (targetEnvironmentConfiguration.isLocal()) PathInfo.localPathInfoProvider else null
return listOfNotNull(validateEnvironmentDirectoryLocation(locationField, provider),
val platformAndRoot = targetEnvironmentConfiguration.getPlatformAndRoot()
return listOfNotNull(validateEnvironmentDirectoryLocation(locationField, platformAndRoot),
validateSdkComboBox(baseInterpreterCombobox, this))
}
else {

View File

@@ -2,17 +2,16 @@
package com.jetbrains.python.sdk.flavors.conda;
import com.intellij.execution.target.TargetEnvironmentConfiguration;
import com.intellij.execution.target.readableFs.PathInfo;
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.pathValidation.PathValidatorKt;
import com.jetbrains.python.pathValidation.PlatformAndRoot;
import com.jetbrains.python.pathValidation.ValidationRequest;
import com.jetbrains.python.sdk.PythonSdkUtil;
import com.jetbrains.python.sdk.add.target.PathValidatorKt;
import com.jetbrains.python.sdk.add.target.ValidationRequest;
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import icons.PythonIcons;
@@ -108,22 +107,15 @@ public final class CondaEnvSdkFlavor extends CPythonSdkFlavor<PyCondaFlavorData>
return PythonIcons.Python.Anaconda;
}
/**
* @deprecated use {@link #validateCondaPath(String, TargetConfigurationReadableFs)}
*/
@Deprecated
public static ValidationInfo validateCondaPath(@Nullable @SystemDependent String condaExecutable) {
return validateCondaPath(condaExecutable, PathInfo.Companion.getLocalPathInfoProvider());
}
@Nullable
@RequiresBackgroundThread
public static ValidationInfo validateCondaPath(@Nullable @SystemDependent String condaExecutable,
@Nullable TargetConfigurationReadableFs pathInfoProvider) {
@NotNull PlatformAndRoot platformAndRoot) {
return PathValidatorKt.validateExecutableFile(
new ValidationRequest(
condaExecutable,
PyBundle.message("python.add.sdk.conda.executable.path.is.empty"),
pathInfoProvider,
platformAndRoot,
null
));
}

View File

@@ -3,9 +3,11 @@ package com.jetbrains.python.sdk.flavors.conda
import com.intellij.execution.target.*
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs
import com.intellij.openapi.project.Project
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
/**
* Encapsulates conda binary command to simplify target request creation
@@ -16,19 +18,16 @@ class PyCondaCommand(
internal val project: Project? = null,
internal val indicator: TargetProgressIndicator = TargetProgressIndicator.EMPTY
) {
@RequiresBackgroundThread
private fun createRequest(): Result<TargetEnvironmentRequest> {
(targetConfig as? TargetConfigurationReadableFs)?.let {
val pathInfo = it.getPathInfo(fullCondaPathOnTarget)
if (pathInfo == null) {
return Result.failure(Exception("$fullCondaPathOnTarget does not exist"))
}
if (pathInfo != PathInfo.Unknown && (pathInfo as? PathInfo.RegularFile)?.executable != true) {
return Result.failure(Exception("$fullCondaPathOnTarget is not executable file"))
}
validateExecutableFile(ValidationRequest(fullCondaPathOnTarget, platformAndRoot = targetConfig.getPlatformAndRoot()))?.let {
return Result.failure(Exception(it.message))
}
return Result.success(targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest())
}
@RequiresBackgroundThread
fun createRequestEnvAndCommandLine(): Result<Triple<TargetEnvironmentRequest, TargetEnvironment, TargetedCommandLineBuilder>> {
val request = createRequest().getOrElse { return Result.failure(it) }

View File

@@ -8,8 +8,10 @@ import com.google.gson.Gson
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.createProcessWithResult
import com.intellij.openapi.progress.*
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.add.target.conda.TargetCommandExecutor
import com.jetbrains.python.ui.pyModalSuspend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
@@ -75,8 +77,10 @@ data class PyCondaEnv(val envIdentity: PyCondaEnvIdentity,
}
suspend fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequest): Result<Process> {
val (_, env, commandLineBuilder) = withContext(Dispatchers.IO) {
command.createRequestEnvAndCommandLine()
}.getOrElse { return Result.failure(it) }
val (_, env, commandLineBuilder) = command.createRequestEnvAndCommandLine().getOrElse { return Result.failure(it) }
val commandLine = commandLineBuilder.apply {
//conda create -y -n myenv python=3.9

View File

@@ -38,7 +38,7 @@ class PyAddNewPipEnvFromFilePanel(private val module: Module) : JPanel() {
add(formPanel, BorderLayout.NORTH)
}
fun validateAll(): List<ValidationInfo> = listOfNotNull(validatePipEnvExecutable(pipEnvPathField.text))
fun validateAll(): List<ValidationInfo> = emptyList() // Pre-target validation is not supported
data class Data(val pipEnvPath: @NlsSafe @SystemDependent String)
}

View File

@@ -139,7 +139,7 @@ class PyAddPipEnvPanel(private val project: Project?,
}
override fun validateAll(): List<ValidationInfo> =
listOfNotNull(validatePipEnvExecutable(), validatePipEnvIsNotAdded())
listOfNotNull(validatePipEnvIsNotAdded())
override fun addChangeListener(listener: Runnable) {
pipEnvPathField.textField.document.addDocumentListener(object : DocumentAdapter() {
@@ -165,10 +165,6 @@ class PyAddPipEnvPanel(private val project: Project?,
private val selectedModule: Module?
get() = module ?: moduleField.selectedItem as? Module
private fun validatePipEnvExecutable(): ValidationInfo? {
return validatePipEnvExecutable(pipEnvPathField.text.nullize() ?: detectPipEnvExecutable()?.absolutePath)
}
/**
* Checks if the pipenv for the project hasn't been already added.
*/

View File

@@ -13,7 +13,6 @@ import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.ide.util.PropertiesComponent
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationListener
@@ -50,8 +49,9 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.PyPackageRequirementsInspection
import com.jetbrains.python.packaging.*
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.target.ValidationRequest
import com.jetbrains.python.sdk.add.target.validateExecutableFile
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import icons.PythonIcons
import org.jetbrains.annotations.SystemDependent
@@ -124,13 +124,6 @@ fun detectPipEnvExecutable(): File? {
fun getPipEnvExecutable(): File? =
PropertiesComponent.getInstance().pipEnvPath?.let { File(it) } ?: detectPipEnvExecutable()
fun validatePipEnvExecutable(pipEnvExecutable: @SystemDependent String?): ValidationInfo? =
validateExecutableFile(ValidationRequest(
path = pipEnvExecutable,
fieldIsEmpty = PyBundle.message("python.sdk.pipenv.executable.not.found"),
pathInfoProvider = PathInfo.localPathInfoProvider // TODO: pass real converter from targets API when we support pip @ targets
))
fun suggestedSdkName(basePath: @NlsSafe String): @NlsSafe String = "Pipenv (${PathUtil.getFileName(basePath)})"
/**

View File

@@ -31,7 +31,6 @@ import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.ItemEvent
import java.io.File
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import javax.swing.Icon
import javax.swing.JComboBox
@@ -130,7 +129,7 @@ class PyAddNewPoetryPanel(private val project: Project?,
override fun getOrCreateSdk(): Sdk? {
PropertiesComponent.getInstance().poetryPath = poetryPathField.text.nullize()
return setupPoetrySdkUnderProgress(project, selectedModule, existingSdks, newProjectPath,
baseSdkField.selectedSdk?.homePath, installPackagesCheckBox.isSelected)?.apply {
baseSdkField.selectedSdk?.homePath, installPackagesCheckBox.isSelected)?.apply {
PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdkField.selectedSdk?.homePath
}
}
@@ -144,7 +143,7 @@ class PyAddNewPoetryPanel(private val project: Project?,
}
override fun validateAll(): List<ValidationInfo> =
listOfNotNull(validatePoetryExecutable(), validatePoetryIsNotAdded())
emptyList() // Pre target validation is not supported
override fun addChangeListener(listener: Runnable) {
poetryPathField.textField.document.addDocumentListener(object : DocumentAdapter() {
@@ -175,21 +174,6 @@ class PyAddNewPoetryPanel(private val project: Project?,
null
} as? Module
/**
* Checks if `poetry` is available on `$PATH`.
*/
private fun validatePoetryExecutable(): ValidationInfo? {
val executable = poetryPathField.text.nullize()?.let { File(it) }
?: detectPoetryExecutable()
?: return ValidationInfo(PyBundle.message("python.sdk.poetry.executable.not.found"))
return when {
!executable.exists() -> ValidationInfo(PyBundle.message("python.sdk.file.not.found", executable.absolutePath))
!Files.isExecutable(executable.toPath()) || !executable.isFile -> ValidationInfo(
PyBundle.message("python.sdk.cannot.execute", executable.absolutePath))
else -> null
}
}
private val isPoetry by lazy { existingSdks.filter { it.isPoetry }.associateBy { it.associatedModulePath } }
private val homePath by lazy { existingSdks.associateBy { it.homePath } }
private val pythonExecutable = ConcurrentHashMap<String, String>()

View File

@@ -11,7 +11,6 @@ import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessNotCreatedException
import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.target.readableFs.PathInfo
import com.intellij.ide.util.PropertiesComponent
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationListener
@@ -57,8 +56,9 @@ import com.jetbrains.python.packaging.PyPackageManagerUI
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.PyAddSdkGroupPanel
import com.jetbrains.python.sdk.add.PyAddSdkPanel
import com.jetbrains.python.sdk.add.target.ValidationRequest
import com.jetbrains.python.sdk.add.target.validateExecutableFile
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.statistics.modules
import icons.PythonIcons
@@ -148,7 +148,7 @@ fun validatePoetryExecutable(poetryExecutable: @SystemDependent String?): Valida
validateExecutableFile(ValidationRequest(
path = poetryExecutable,
fieldIsEmpty = PyBundle.message("python.sdk.poetry.executable.not.found"),
pathInfoProvider = PathInfo.localPathInfoProvider // TODO: pass real converter from targets when we support poetry @ targets
platformAndRoot = PlatformAndRoot.local // TODO: pass real converter from targets when we support poetry @ targets
))

View File

@@ -0,0 +1,26 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.ui
import com.intellij.openapi.progress.ModalTaskOwner
import com.intellij.openapi.progress.TaskCancellation
import com.intellij.openapi.progress.runWithModalProgressBlocking
import com.intellij.openapi.progress.withModalProgress
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
/**
* Runs [code] in background under the modal dialog
*/
@RequiresEdt
@RequiresBlockingContext
fun <T> pyModalBlocking(modalTaskOwner: ModalTaskOwner = ModalTaskOwner.guess(), code: () -> T): T =
runWithModalProgressBlocking(modalTaskOwner, PySdkBundle.message("python.sdk.run.wait"), TaskCancellation.nonCancellable()) {
code.invoke()
}
suspend fun <T> pyModalSuspend(modalTaskOwner: ModalTaskOwner = ModalTaskOwner.guess(), code: () -> T): T =
withModalProgress(modalTaskOwner, PySdkBundle.message("python.sdk.run.wait"), TaskCancellation.nonCancellable()) {
code.invoke()
}

View File

@@ -1,27 +1,29 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.ui.targetPathEditor
import com.intellij.execution.Platform
import com.intellij.execution.target.*
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.io.OSAgnosticPathUtil
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.toMutableProperty
import com.jetbrains.python.PyBundle
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import com.jetbrains.python.ui.pyModalBlocking
import java.util.function.Supplier
import javax.swing.JComponent
/**
* The dialog that allows to specify the path to a file or directory manually.
*
* Performs validation that the path that is being added is an absolute path on
* the specified [platform].
*
* It must be used for remote FS only. No local FS supported.
*/
class ManualPathEntryDialog(private val project: Project?,
private val platform: Platform = Platform.UNIX,
targetConfig: TargetEnvironmentConfiguration? = null) : DialogWrapper(project) {
targetConfig: TargetEnvironmentConfiguration? = null)
: DialogWrapper(project) {
private val targetConfigAndType: Pair<TargetEnvironmentConfiguration, BrowsableTargetEnvironmentType>? =
(targetConfig?.getTargetType() as? BrowsableTargetEnvironmentType)?.let { Pair(targetConfig, it) }
@@ -37,29 +39,20 @@ class ManualPathEntryDialog(private val project: Project?,
val label = PyBundle.message("path.label")
return panel {
row(label = label) {
val textFieldComponent = if (targetConfigAndType == null)
val textFieldComponent = if (targetConfigAndType == null || project == null)
textField().bindText(::path)
else
textFieldWithBrowseTargetButton(targetConfigAndType.second, Supplier { targetConfigAndType.first }, project!!, label, this@ManualPathEntryDialog::path.toMutableProperty(), TargetBrowserHints(true))
textFieldWithBrowseTargetButton(targetConfigAndType.second, Supplier { targetConfigAndType.first }, project, label,
this@ManualPathEntryDialog::path.toMutableProperty(), TargetBrowserHints(true))
textFieldComponent.validationOnApply { textField ->
val text = textField.text
when {
text.isBlank() -> error(PyBundle.message("path.must.not.be.empty.error.message"))
!isAbsolutePath(text, platform) -> error(PyBundle.message("path.must.be.absolute.error.message"))
text.endsWith(" ") -> warning(PyBundle.message("path.ends.with.whitespace.warning.message"))
else -> null
return@validationOnApply pyModalBlocking {
// this dialog is always for remote
validateExecutableFile(
ValidationRequest(text, platformAndRoot = targetConfigAndType?.first.getPlatformAndRoot(defaultIsLocal = false)))
}
}.focused()
}
}
}
companion object {
fun isAbsolutePath(path: String, platform: Platform): Boolean = when (platform) {
Platform.UNIX -> path.startsWith("/")
Platform.WINDOWS -> isAbsoluteWindowsPath(path)
}
private fun isAbsoluteWindowsPath(path: String): Boolean = OSAgnosticPathUtil.isAbsoluteDosPath(path)
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.env
import com.intellij.testFramework.RuleChain
import com.jetbrains.env.python.PySDKRule
import org.junit.Rule
class PySdkFlavorLocalTest : PySdkFlavorTestBase() {
override val sdkRule: PySDKRule = PySDKRule(null)
@Rule
@JvmField
val ruleChain: RuleChain = RuleChain(projectRule, sdkRule)
}

View File

@@ -0,0 +1,32 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.env
import com.intellij.testFramework.ProjectRule
import com.jetbrains.env.python.PySDKRule
import com.jetbrains.python.sdk.getPythonBinaryPath
import com.jetbrains.python.sdk.sdkSeemsValid
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
import kotlin.time.Duration.Companion.minutes
/**
* Tests sdk flavor
* * Extend this class
* * Implement [sdkRule]
* * Use [com.intellij.testFramework.RuleChain] with [projectRule], [sdkRule] e.t.c as you do in JUnit4 test
*/
abstract class PySdkFlavorTestBase {
protected val projectRule = ProjectRule()
protected abstract val sdkRule: PySDKRule
@Test
fun testValid(): Unit = runTest(timeout = 2.minutes) {
sdkRule.sdk.getPythonBinaryPath(projectRule.project).getOrThrow()
repeat(1000) {
Assert.assertTrue(sdkRule.sdk.sdkSeemsValid)
}
}
}

View File

@@ -1,81 +0,0 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.ui
import com.intellij.execution.Platform
import com.jetbrains.python.ui.targetPathEditor.ManualPathEntryDialog
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class ManualPathEntryDialogTest {
@Parameterized.Parameter(0)
@JvmField
var path: String? = null
@Parameterized.Parameter(1)
@JvmField
var platform: Platform? = null
@Parameterized.Parameter(2)
@JvmField
var isAbsolute: Boolean = false
@Test
fun `test isAbsolutePath`() {
Assert.assertEquals(ManualPathEntryDialog.isAbsolutePath(path!!, platform!!), isAbsolute)
}
companion object {
@Parameterized.Parameters(name = "[{1}] Path ''{0}'' is absolute == {2}")
@JvmStatic
fun data() = arrayOf(
// Unix absolute paths
arrayOf("/", Platform.UNIX, true),
arrayOf("/opt", Platform.UNIX, true),
arrayOf("/opt/", Platform.UNIX, true),
arrayOf("/opt/project", Platform.UNIX, true),
arrayOf("/opt/project/", Platform.UNIX, true),
arrayOf("//", Platform.UNIX, true),
arrayOf("/opt//project", Platform.UNIX, true),
arrayOf("/opt//project//", Platform.UNIX, true),
// Unix relative paths
arrayOf(".", Platform.UNIX, false),
arrayOf("./", Platform.UNIX, false),
arrayOf("opt/", Platform.UNIX, false),
arrayOf("opt/project", Platform.UNIX, false),
arrayOf("opt/project/", Platform.UNIX, false),
arrayOf("./opt/", Platform.UNIX, false),
arrayOf("./opt/project", Platform.UNIX, false),
arrayOf("./opt/project/", Platform.UNIX, false),
// Windows absolute paths
arrayOf("C:\\", Platform.WINDOWS, true),
arrayOf("C:/", Platform.WINDOWS, true),
arrayOf("c:\\", Platform.WINDOWS, true),
arrayOf("c:/", Platform.WINDOWS, true),
arrayOf("C:/opt/", Platform.WINDOWS, true),
arrayOf("C:/opt/project", Platform.WINDOWS, true),
arrayOf("C:/opt/project/", Platform.WINDOWS, true),
arrayOf("C:\\opt\\", Platform.WINDOWS, true),
arrayOf("C:\\opt\\project", Platform.WINDOWS, true),
arrayOf("C:\\opt\\project\\", Platform.WINDOWS, true),
// Windows relative paths
arrayOf("opt/", Platform.WINDOWS, false),
arrayOf("opt/project", Platform.WINDOWS, false),
arrayOf("opt/project/", Platform.WINDOWS, false),
arrayOf("./opt/", Platform.WINDOWS, false),
arrayOf("./opt/project", Platform.WINDOWS, false),
arrayOf("./opt/project/", Platform.WINDOWS, false),
arrayOf("opt\\", Platform.WINDOWS, false),
arrayOf("opt\\project", Platform.WINDOWS, false),
arrayOf("opt\\project\\", Platform.WINDOWS, false),
arrayOf(".\\opt\\", Platform.WINDOWS, false),
arrayOf(".\\opt\\project", Platform.WINDOWS, false),
arrayOf(".\\opt\\project\\", Platform.WINDOWS, false),
)
}
}