diff --git a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt index 040a5b3a14df..e39d3fc282e8 100644 --- a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt @@ -15,24 +15,20 @@ import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonRepositoryManager import com.jetbrains.python.packaging.pip.PipRepositoryManager -import com.jetbrains.python.sdk.associatedModulePath import com.jetbrains.python.sdk.pipenv.PIP_FILE -import com.jetbrains.python.sdk.pipenv.runPipEnv +import com.jetbrains.python.sdk.pipenv.runPipEnvWithSdk import java.nio.file.Path import com.jetbrains.python.sdk.pipenv.PipEnvParser as SdkPipEnvParser internal class PipEnvPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) { - private val modulePath: Path? - get() = sdk.associatedModulePath?.let { Path.of(it) } - override val repositoryManager: PythonRepositoryManager = PipRepositoryManager.getInstance(project) override suspend fun syncLockedCommand(): PyResult { - return runPipEnv(modulePath, "install", "--dev").mapSuccess { } + return runPipEnvWithSdk(sdk, "install", "--dev").mapSuccess { } } suspend fun lock(): PyResult { - return runPipEnv(modulePath, "lock").mapSuccess { } + return runPipEnvWithSdk(sdk, "lock").mapSuccess { } } override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List, module: Module?, dependencyGroup: PyDependencyGroup?): PyResult { @@ -41,24 +37,24 @@ internal class PipEnvPackageManager(project: Project, sdk: Sdk) : PythonPackageM is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> { val args = listOf("install") + installRequest.specifications.map { it.nameWithVersionSpec } + options - runPipEnv(modulePath, *args.toTypedArray()).mapSuccess { } + runPipEnvWithSdk(sdk, *args.toTypedArray()).mapSuccess { } } } } override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): PyResult { val args = listOf("install") + specifications.map { it.nameWithVersionSpec } - return runPipEnv(modulePath, *args.toTypedArray()).mapSuccess { } + return runPipEnvWithSdk(sdk, *args.toTypedArray()).mapSuccess { } } override suspend fun uninstallPackageCommand(vararg pythonPackages: String, workspaceMember: PyWorkspaceMember?, dependencyGroup: PyDependencyGroup?): PyResult { val args = listOf("uninstall") + pythonPackages.toList() - return runPipEnv(modulePath, *args.toTypedArray()).mapSuccess { } + return runPipEnvWithSdk(sdk, *args.toTypedArray()).mapSuccess { } } override suspend fun loadPackagesCommand(): PyResult> { - val output = runPipEnv(modulePath, "graph", "--json") + val output = runPipEnvWithSdk(sdk, "graph", "--json") output.getOr { return it } @@ -72,7 +68,7 @@ internal class PipEnvPackageManager(project: Project, sdk: Sdk) : PythonPackageM } override suspend fun loadOutdatedPackagesCommand(): PyResult> { - val output = runPipEnv(modulePath, "update", "--dry-run").getOr { return it } + val output = runPipEnvWithSdk(sdk, "update", "--dry-run").getOr { return it } val outdated = PipEnvParser.parseOutdatedPackagesOutput(output) return PyResult.success(outdated) @@ -87,4 +83,4 @@ internal class PipEnvPackageManager(project: Project, sdk: Sdk) : PythonPackageM override val dependenciesFilesRelativePaths: List get() = listOf(Path.of(PIP_FILE)) -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 63da6d29c18f..f17ab4be75d5 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -145,7 +145,7 @@ enum class PythonSupportedEnvironmentManagers( VIRTUALENV(VENV_TOOL_ID, "sdk.create.custom.virtualenv", PythonVenvIcons.VirtualEnv, sshAutoUploadRequired = false, { true }), CONDA(CONDA_TOOL_ID, "sdk.create.custom.conda", PythonCommunityImplCondaIcons.Anaconda, sshAutoUploadRequired = false, { true }), POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry, sshAutoUploadRequired = true, { true }), - PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PythonCommunityImplPipenvIcons.PythonClosed, sshAutoUploadRequired = false), + PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PythonCommunityImplPipenvIcons.PythonClosed, sshAutoUploadRequired = true, { true }), UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonUvCommonIcons.UV, sshAutoUploadRequired = true, { true }), HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, sshAutoUploadRequired = true, { true }), PYTHON(VENV_TOOL_ID, "sdk.create.custom.python", PythonParserIcons.PythonFile, sshAutoUploadRequired = false, { true }) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/pipenv/EnvironmentCreatorPip.kt b/python/src/com/jetbrains/python/sdk/add/v2/pipenv/EnvironmentCreatorPip.kt index 1d72198b364c..07256f6d1213 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/pipenv/EnvironmentCreatorPip.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/pipenv/EnvironmentCreatorPip.kt @@ -8,7 +8,6 @@ import com.intellij.python.community.impl.pipenv.PipEnvPyTool import com.intellij.python.community.impl.pipenv.pipenvPath import com.intellij.python.pytools.PyTool import com.intellij.platform.util.progress.withProgressText -import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle.message import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyResult @@ -34,16 +33,19 @@ internal class EnvironmentCreatorPip

(model: PythonMutableTargetA override suspend fun setupEnvSdk(moduleBasePath: Path): PyResult { val basePythonBinaryPath = model.getOrInstallBasePython() + ?: return PyResult.localizedError(message("python.sdk.provided.path.is.invalid", null)) + val pipenvExecutable = model.pipenvViewModel.pipenvExecutable.get()?.pathHolder + ?: return PyResult.localizedError(message("python.sdk.provided.path.is.invalid", null)) - return when (basePythonBinaryPath) { - is PathHolder.Eel -> withProgressText(message("python.sdk.progress.pipenv.creating")) { - setupPipEnvSdkWithProgressReport( - moduleBasePath = moduleBasePath, - basePythonBinaryPath = basePythonBinaryPath.path, - installPackages = false - ) - } - else -> PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath)) + return withProgressText(message("python.sdk.progress.pipenv.creating")) { + setupPipEnvSdkWithProgressReport( + moduleBasePath = moduleBasePath, + basePythonBinaryPath = basePythonBinaryPath, + fileSystem = model.fileSystem, + pipenvExecutable = pipenvExecutable, + installPackages = false, + targetPanelExtension = model.state.targetPanelExtension.get(), + ) } } } diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipEnvPipFileWatcher.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipEnvPipFileWatcher.kt index e51b9248bb47..c18354d2ca2b 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PipEnvPipFileWatcher.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipEnvPipFileWatcher.kt @@ -25,7 +25,6 @@ import com.jetbrains.python.errorProcessing.emit import com.jetbrains.python.onFailure import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.associatedModuleDir -import com.jetbrains.python.sdk.associatedModulePath import com.jetbrains.python.sdk.findAmongRoots import com.jetbrains.python.sdk.pythonSdk import com.jetbrains.python.sdk.skeleton.PySkeletonUtil @@ -33,7 +32,6 @@ import com.jetbrains.python.statistics.PipfileWatcherIdsHolder.Companion.RUN_PIP import com.jetbrains.python.errorProcessing.ErrorSink import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.nio.file.Path /** * Watches for edits in Pipfiles inside modules with a pipenv SDK set. @@ -114,7 +112,7 @@ internal class PipEnvPipFileWatcher : EditorFactoryListener { PyPackageCoroutine.launch(module.project) { withBackgroundProgress(module.project, description) { val sdk = module.pythonSdk ?: return@withBackgroundProgress - runPipEnv(sdk.associatedModulePath?.let { Path.of(it) }, *args.toTypedArray()).onFailure { + runPipEnvWithSdk(sdk, *args.toTypedArray()).onFailure { ErrorSink().emit(it, module.project) } @@ -139,4 +137,4 @@ internal class PipEnvPipFileWatcher : EditorFactoryListener { get() = FileDocumentManager.getInstance().getFile(this) private fun VirtualFile.getModule(project: Project): Module? = ModuleUtil.findModuleForFile(this, project) -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt index 9d192282045c..73e61c87462c 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt @@ -1,9 +1,11 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.pipenv +import com.intellij.openapi.components.service import com.intellij.openapi.projectRoots.Sdk import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.localEel +import com.intellij.python.community.execService.DownloadConfig import com.intellij.python.community.execService.ProcessOutputTransformer import com.intellij.python.community.impl.pipenv.pipenvPath import com.jetbrains.python.PyBundle @@ -13,33 +15,52 @@ import com.jetbrains.python.sdk.ToolCommandExecutor import com.jetbrains.python.sdk.add.v2.EelFileSystem import com.jetbrains.python.sdk.add.v2.FileSystem import com.jetbrains.python.sdk.add.v2.PathHolder +import com.jetbrains.python.sdk.add.v2.TargetFileSystemCache import com.jetbrains.python.sdk.add.v2.toEelFileSystem -import com.jetbrains.python.sdk.createSdk +import com.jetbrains.python.sdk.impl.PySdkBundle +import com.jetbrains.python.sdk.pySdkAdditionalData import com.jetbrains.python.sdk.runTool -import com.jetbrains.python.venvReader.VirtualEnvReader -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import com.jetbrains.python.target.PyTargetAwareAdditionalData +import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration +import com.jetbrains.python.target.ui.TargetPanelExtension import org.jetbrains.annotations.ApiStatus.Internal import org.jetbrains.annotations.SystemDependent import java.nio.file.Path import kotlin.io.path.createFile import kotlin.io.path.exists -import kotlin.io.path.pathString internal val PIPENV_TOOL: ToolCommandExecutor = ToolCommandExecutor("pipenv") { pipenvPath } +private val PIPENV_PROJECT_DOWNLOAD_CONFIG = DownloadConfig(relativePaths = listOf(PIP_FILE, PIP_FILE_LOCK)) +private val PIPENV_PROJECT_MUTATING_COMMANDS = setOf("--python", "install", "lock", "sync", "uninstall", "update") + +internal suspend fun

runPipEnv( + fileSystem: FileSystem

, + dirPath: Path?, + vararg args: String, + pipenvExecutable: P? = null, + baseEnv: Map = emptyMap(), + downloadConfig: DownloadConfig? = null, +): PyResult = + PIPENV_TOOL.runTool( + fileSystem = fileSystem, + pathFromSdk = pipenvExecutable?.toString(), + dirPath = dirPath, + args = args, + env = baseEnv, + downloadConfig = downloadConfig, + ) + @Internal suspend fun runPipEnv(dirPath: Path?, vararg args: String): PyResult = - PIPENV_TOOL.runTool( + runPipEnv( fileSystem = dirPath.toEelFileSystem(), - pathFromSdk = null, dirPath = dirPath, args = args, ) -@Internal internal suspend fun runPipEnv(dirPath: Path?, vararg args: String, transformer: ProcessOutputTransformer): PyResult = PIPENV_TOOL.runTool( fileSystem = dirPath.toEelFileSystem(), @@ -52,39 +73,88 @@ internal suspend fun runPipEnv(dirPath: Path?, vararg args: String, transfor /** * Returns the configured pipenv executable or detects it automatically on the given [fileSystem]. */ -@Internal internal suspend fun

getPipEnvExecutable(fileSystem: FileSystem

): P? = PIPENV_TOOL.getToolExecutable(fileSystem, pathFromSdk = null) /** * Returns the configured pipenv executable or detects it automatically. */ -@Internal internal suspend fun getPipEnvExecutable(eel: EelApi = localEel): Path? = getPipEnvExecutable(EelFileSystem(eel))?.path +internal suspend fun runPipEnvWithSdk(sdk: Sdk, vararg args: String): PyResult { + val data = sdk.pySdkAdditionalData + val workingDirectory = data.workingDirectory.takeIf { data.hasValidWorkingDirectory() } + ?: return PyResult.localizedError(PyBundle.message("python.sdk.project.working.directory.not.found")) + val sdkHomePath = sdk.homePath + ?: return PyResult.localizedError(PySdkBundle.message("python.sdk.broken.configuration", sdk.name)) + + return when (data) { + is PyTargetAwareAdditionalData -> { + val targetConfig = data.targetEnvironmentConfiguration + ?: return PyResult.localizedError(PySdkBundle.message("python.sdk.broken.configuration", sdk.name)) + runPipEnvWithSdk( + fileSystem = service().getOrCreate(targetConfig, PythonLanguageRuntimeConfiguration()), + workingDirectory = workingDirectory, + sdkHomePath = sdkHomePath, + args = args, + ) + } + else -> runPipEnvWithSdk( + fileSystem = workingDirectory.toEelFileSystem(), + workingDirectory = workingDirectory, + sdkHomePath = sdkHomePath, + args = args, + ) + } +} + +private suspend fun

runPipEnvWithSdk( + fileSystem: FileSystem

, + workingDirectory: Path, + sdkHomePath: String, + vararg args: String, +): PyResult { + val pythonPath = fileSystem.parsePath(sdkHomePath).getOr { return it } + val pythonHomePath = fileSystem.resolvePythonHome(pythonPath) + return runPipEnv( + fileSystem = fileSystem, + dirPath = workingDirectory, + args = args, + baseEnv = mapOf("VIRTUAL_ENV" to pythonHomePath.toString()), + downloadConfig = PIPENV_PROJECT_DOWNLOAD_CONFIG.takeIf { args.firstOrNull() in PIPENV_PROJECT_MUTATING_COMMANDS }, + ) +} + /** - * Sets up the pipenv environment under the modal progress window. - * - * The pipenv is associated with the first valid object from this list: - * - * 1. New project specified by [newProjectPath] - * 2. Existing module specified by [module] - * 3. Existing project specified by [project] + * Sets up the pipenv environment for [moduleBasePath] and creates its SDK. * * @return the SDK for pipenv, not stored in the SDK table yet. */ -@Internal -internal suspend fun setupPipEnvSdkWithProgressReport( +internal suspend fun

setupPipEnvSdkWithProgressReport( moduleBasePath: Path, - basePythonBinaryPath: PythonBinary?, + basePythonBinaryPath: P?, + fileSystem: FileSystem

, + pipenvExecutable: P?, installPackages: Boolean, + targetPanelExtension: TargetPanelExtension? = null, ): PyResult { - val pythonExecutablePath = setUpPipEnv(moduleBasePath, basePythonBinaryPath, installPackages).getOr { return it } + val pythonHomePath = setupPipEnv( + projectPath = moduleBasePath, + fileSystem = fileSystem, + pipenvExecutable = pipenvExecutable, + basePythonBinaryPath = basePythonBinaryPath, + installPackages = installPackages, + ).getOr { return it } + val pythonBinaryPath = fileSystem.resolvePythonBinary(pythonHomePath) + ?: return PyResult.localizedError(PyBundle.message("python.sdk.cannot.setup.sdk", pythonHomePath)) - return createSdk( - pythonBinaryPath = PathHolder.Eel(pythonExecutablePath), - sdkAdditionalData = PyPipEnvSdkAdditionalData(moduleBasePath) + return fileSystem.setupSdk( + project = null, + pythonBinaryPath = pythonBinaryPath, + sdkAdditionalData = PyPipEnvSdkAdditionalData(moduleBasePath), + targetPanelExtension = targetPanelExtension, + suggestedSdkName = null, ) } @@ -93,12 +163,26 @@ internal suspend fun setupPipEnvSdkWithProgressReport( * * @return the path to the pipenv environment. */ -@Internal internal suspend fun setupPipEnv( projectPath: Path, basePythonBinaryPath: PythonBinary?, installPackages: Boolean, -): PyResult<@SystemDependent String> { +): PyResult<@SystemDependent String> = + setupPipEnv( + projectPath = projectPath, + fileSystem = projectPath.toEelFileSystem(), + pipenvExecutable = null, + basePythonBinaryPath = basePythonBinaryPath?.let(PathHolder::Eel), + installPackages = installPackages, + ).mapSuccess { it.toString() } + +internal suspend fun

setupPipEnv( + projectPath: Path, + fileSystem: FileSystem

, + pipenvExecutable: P?, + basePythonBinaryPath: P?, + installPackages: Boolean, +): PyResult

{ val pipfile = projectPath.resolve(PIP_FILE) if (!pipfile.exists()) { @@ -111,22 +195,37 @@ internal suspend fun setupPipEnv( when { installPackages -> { - val pythonArgs = if (basePythonBinaryPath != null) listOf("--python", basePythonBinaryPath.pathString) else emptyList() + val pythonArgs = if (basePythonBinaryPath != null) listOf("--python", basePythonBinaryPath.toString()) else emptyList() val command = pythonArgs + listOf("install", "--dev") - runPipEnv(projectPath, *command.toTypedArray()).getOr { return it } + runPipEnv( + fileSystem = fileSystem, + dirPath = projectPath, + args = command.toTypedArray(), + pipenvExecutable = pipenvExecutable, + downloadConfig = PIPENV_PROJECT_DOWNLOAD_CONFIG, + ).getOr { return it } } basePythonBinaryPath != null -> - runPipEnv(projectPath, "--python", basePythonBinaryPath.pathString).getOr { return it } + runPipEnv( + fileSystem = fileSystem, + dirPath = projectPath, + "--python", basePythonBinaryPath.toString(), + pipenvExecutable = pipenvExecutable, + downloadConfig = PIPENV_PROJECT_DOWNLOAD_CONFIG, + ).getOr { return it } else -> - runPipEnv(projectPath, "run", "python", "-V").getOr { return it } + runPipEnv( + fileSystem = fileSystem, + dirPath = projectPath, + "run", "python", "-V", + pipenvExecutable = pipenvExecutable, + ).getOr { return it } } - return runPipEnv(projectPath, "--venv") -} - -private suspend fun setUpPipEnv(moduleBasePath: Path, basePythonBinaryPath: PythonBinary?, installPackages: Boolean): PyResult { - val pipEnv = setupPipEnv(moduleBasePath, basePythonBinaryPath, installPackages).getOr { return it } - val pipEnvExecutablePathString = withContext(Dispatchers.IO) { - VirtualEnvReader().findPythonInPythonRoot(Path.of(pipEnv))?.toString() - } ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", pipEnv)) - return PyResult.success(Path.of(pipEnvExecutablePathString)) + val pythonHomePath = runPipEnv( + fileSystem = fileSystem, + dirPath = projectPath, + "--venv", + pipenvExecutable = pipenvExecutable, + ).getOr { return it } + return fileSystem.parsePath(pythonHomePath.trim()) } diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt index cd9af77d000d..13b3a1ca6133 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt @@ -5,11 +5,13 @@ import com.intellij.python.community.impl.pipenv.PipEnvPyTool import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor import com.jetbrains.python.sdk.flavors.PyFlavorData import com.jetbrains.python.sdk.flavors.PythonFlavorProvider +import org.jetbrains.annotations.ApiStatus import java.nio.file.Path import javax.swing.Icon -internal object PyPipEnvSdkFlavor : CPythonSdkFlavor() { +@ApiStatus.Internal +object PyPipEnvSdkFlavor : CPythonSdkFlavor() { override fun getIcon(): Icon = PipEnvPyTool.getInstance().icon override fun getFlavorDataClass(): Class = PyFlavorData.Empty::class.java @@ -19,4 +21,4 @@ internal object PyPipEnvSdkFlavor : CPythonSdkFlavor() { internal class PyPipEnvSdkFlavorProvider : PythonFlavorProvider { override fun getFlavor(): PyPipEnvSdkFlavor = PyPipEnvSdkFlavor -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt b/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt index 3c9c5cbc5c10..ca29b0e31771 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt @@ -5,19 +5,14 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.NlsSafe import com.intellij.util.PathUtil import com.jetbrains.python.sdk.legacy.PythonSdkUtil +import com.jetbrains.python.sdk.pySdkAdditionalData import org.jetbrains.annotations.ApiStatus.Internal /** * Tells if the SDK was added as a pipenv. */ internal val Sdk.isPipEnv: Boolean - get() { - if (!PythonSdkUtil.isPythonSdk(this)) { - return false - } - - return sdkAdditionalData is PyPipEnvSdkAdditionalData - } + get() = PythonSdkUtil.isPythonSdk(this) && pySdkAdditionalData.flavor == PyPipEnvSdkFlavor @Internal -fun suggestedSdkName(basePath: @NlsSafe String): @NlsSafe String = "Pipenv (${PathUtil.getFileName(basePath)})" \ No newline at end of file +fun suggestedSdkName(basePath: @NlsSafe String): @NlsSafe String = "Pipenv (${PathUtil.getFileName(basePath)})"