mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
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:
committed by
intellij-monorepo-bot
parent
40ffdcfa14
commit
2bb2ea60fc
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
}
|
||||
@@ -79,4 +79,11 @@ public final class ValidationInfo {
|
||||
this.okEnabled == that.okEnabled &&
|
||||
this.warning == that.warning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ValidationInfo{" +
|
||||
"message='" + message + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
/**
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)})"
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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
|
||||
|
||||
))
|
||||
|
||||
|
||||
26
python/src/com/jetbrains/python/ui/ModalUtil.kt
Normal file
26
python/src/com/jetbrains/python/ui/ModalUtil.kt
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
14
python/testSrc/com/jetbrains/env/PySdkFlavorLocalTest.kt
vendored
Normal file
14
python/testSrc/com/jetbrains/env/PySdkFlavorLocalTest.kt
vendored
Normal 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)
|
||||
}
|
||||
32
python/testSrc/com/jetbrains/env/PySdkFlavorTest.kt
vendored
Normal file
32
python/testSrc/com/jetbrains/env/PySdkFlavorTest.kt
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user