[python] PY-83042 Fix sync action for remote interpreters

This change disables sync actions for read-only SDKs (e.g. Docker).
Also, when syncing requirements.txt target path is used instead of a
local context (using ExecService). Finally, proper conversion of paths
is done for WSL when running conda.

GitOrigin-RevId: 1489b2325207978ed50f0716337423bfdb7705d2
This commit is contained in:
Alexey Katsman
2025-10-23 11:10:47 +00:00
committed by intellij-monorepo-bot
parent 510344a7de
commit edc0a3cc45
6 changed files with 46 additions and 40 deletions
@@ -21,6 +21,7 @@ import com.jetbrains.python.psi.PyImportStatement
import com.jetbrains.python.psi.PyQualifiedExpression
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.sdk.isReadOnly
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import org.jetbrains.annotations.ApiStatus
@@ -98,7 +99,10 @@ class PyRequirementVisitor(
val message = PyPsiBundle.message(REQUIREMENT_NOT_SATISFIED, requirementsList, unsatisfied.size)
val ignoreFix = IgnoreRequirementFix(unsatisfied.mapTo(mutableSetOf()) { it.presentableTextWithoutVersion })
val quickFixes = listOf(SyncProjectQuickFix(), ignoreFix)
val quickFixes = buildList {
if (!sdk.isReadOnly) add(SyncProjectQuickFix())
add(ignoreFix)
}
registerProblem(
file,
@@ -5,6 +5,7 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.python.community.execService.Args
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.python.HelperName
import com.jetbrains.python.errorProcessing.PyResult
@@ -58,7 +59,7 @@ class PipPackageManagerEngine(
suspend fun syncRequirementsTxt(file: VirtualFile): PyResult<Unit> {
return runPackagingTool(
operation = "install",
arguments = listOf("-r", file.path)
arguments = Args("-r").addLocalFile(file.toNioPath())
).mapSuccess { }
}
@@ -81,15 +82,14 @@ class PipPackageManagerEngine(
return PyResult.success(packages)
}
suspend fun runPackagingTool(operation: String, arguments: List<String>): PyResult<String> = withContext(Dispatchers.IO) {
val parameters = mutableListOf(operation)
private suspend fun runPackagingTool(operation: String, arguments: Args): PyResult<String> = withContext(Dispatchers.IO) {
val parameters = Args(operation)
if (operation == "install") {
PyProxyUtils.proxyString?.let {
parameters += "--proxy"
parameters += it
parameters.addArgs("--proxy", it)
}
}
parameters += arguments
parameters.add(arguments)
thisLogger().debug("Running python packaging tool. Operation: $operation")
ExecService().executeHelper(
@@ -99,6 +99,9 @@ class PipPackageManagerEngine(
)
}
private suspend fun runPackagingTool(operation: String, arguments: List<String>): PyResult<String> =
runPackagingTool(operation, Args(*arguments.toTypedArray()))
private fun partitionPackagesBySource(installRequest: PythonPackageInstallRequest): List<List<String>> {
when (installRequest) {
@@ -10,6 +10,7 @@ import com.jetbrains.python.packaging.management.getPythonPackageManager
import com.jetbrains.python.packaging.pip.PipPythonPackageManager
import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils
import com.jetbrains.python.requirements.RequirementsFileType
import com.jetbrains.python.sdk.isReadOnly
internal sealed class PipPackageManagerAction : PythonPackageManagerAction<PipPythonPackageManager, String>() {
override fun isWatchedFile(virtualFile: VirtualFile?): Boolean {
@@ -51,4 +52,14 @@ internal class PipUpdateEnvAction() : PipPackageManagerAction() {
val currentFile = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return PyResult.success(Unit)
return manager.syncRequirementsTxt(currentFile)
}
override fun update(e: AnActionEvent) {
super.update(e)
if (!e.presentation.isEnabledAndVisible)
return
val manager = e.getPythonPackageManager<PipPythonPackageManager>() ?: return
if (manager.sdk.isReadOnly) {
e.presentation.isEnabledAndVisible = false
}
}
}
@@ -10,12 +10,16 @@ import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.isCondaVirtualEnv
import com.jetbrains.python.onSuccess
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.sdk.conda.*
import com.jetbrains.python.sdk.conda.createCondaSdkAlongWithNewEnv
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.persist
import com.jetbrains.python.sdk.setAssociationToModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.takeWhile
@@ -24,7 +28,7 @@ internal fun PythonAddInterpreterModel<*>.createCondaCommand(): PyResult<PyConda
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
val executable = condaViewModel.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
return PyCondaCommand(
fullCondaPathOnTarget = executable.pathHolder.toString().convertToPathOnTarget(targetEnvironmentConfiguration),
fullCondaPathOnTarget = executable.pathHolder.toString(),
targetConfig = targetEnvironmentConfiguration
).let { PyResult.success(it) }
}
@@ -1,28 +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.jetbrains.python.sdk.add.v2
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import java.nio.file.Path
internal fun String.convertToPathOnTarget(target: TargetEnvironmentConfiguration?): String = Path.of(this).convertToPathOnTarget(target)
internal fun Path.convertToPathOnTarget(target: TargetEnvironmentConfiguration?): String {
val mapper = target?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) }
return mapper?.getTargetPath(this) ?: toString()
}
internal fun FullPathOnTarget.toLocalPathOn(target: TargetEnvironmentConfiguration?): Path {
val mapper = target?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) }
return mapper?.getLocalPath(this) ?: Path.of(this)
}
internal fun String.virtualFileOnTarget(target: TargetEnvironmentConfiguration? = null): VirtualFile? {
if (target == null) return StandardFileSystems.local().findFileByPath(this)
val path = Path.of(this)
val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(target) ?: return null
return mapper.getVfsFromTargetPath(mapper.getTargetPath(path)!!)
}
@@ -48,8 +48,20 @@ suspend fun ExecService.executeHelper(
helperArgs: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyResult<String> =
execGetStdout(sdk, Args().addHelper(helper).addArgs(helperArgs), options, procListener)
): PyResult<String> = executeHelper(sdk, helper, Args(*helperArgs.toTypedArray()), options, procListener)
/**
* Executes [helper] on [sdk] (copies it to the remote machine if needed)
*/
@ApiStatus.Internal
@CheckReturnValue
suspend fun ExecService.executeHelper(
sdk: Sdk,
helper: HelperName,
helperArgs: Args = Args(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyResult<String> = execGetStdout(sdk, Args().addHelper(helper).add(helperArgs), options, procListener)
// See function it calls for more info