= emptyMap(),
+)
diff --git a/python/python-uv/tests/testSrc/com/intellij/python/junit5Tests/env/tests/uv/UvCliTest.kt b/python/python-uv/tests/testSrc/com/intellij/python/junit5Tests/env/tests/uv/UvCliTest.kt
index 2acd5bb28695..cd3de84e8574 100644
--- a/python/python-uv/tests/testSrc/com/intellij/python/junit5Tests/env/tests/uv/UvCliTest.kt
+++ b/python/python-uv/tests/testSrc/com/intellij/python/junit5Tests/env/tests/uv/UvCliTest.kt
@@ -113,7 +113,7 @@ class UvCliTest {
@Test
fun testTool(): Unit = timeoutRunBlocking(60.seconds) {
val tool = myRuntime.uvCli().tool()
- assertTrue(tool.dir().getOrThrow().isNotBlank())
+ assertTrue(tool.dir().getOrThrow().isAbsolute)
tool.list().getOrThrow()
}
@@ -141,21 +141,21 @@ class UvCliTest {
"expected ${pkg.name} install under the class-scoped UV_TOOL_DIR ${uvContext.uvToolDirPath}, missing: $expectedToolEnv"
}
- // 2. listInstalled() should surface the freshly installed tool at the pinned version.
- val installed = tool.listInstalled().getOrThrow()
+ // 2. list(showPaths) should surface the freshly installed tool at the pinned version.
+ val installed = tool.list(showPaths = true).getOrThrow()
val installedEntry = installed.firstOrNull { it.name == pkg.name }
- assertNotNull(installedEntry) { "expected ${pkg.name} in listInstalled(), got $installed" }
+ assertNotNull(installedEntry) { "expected ${pkg.name} in list(), got $installed" }
assertEquals(pkg.version, installedEntry!!.version) {
"expected ${pkg.spec()} right after install, got ${installedEntry.version}"
}
- // 3. listOutdated() should report it with a newer latestVersion (we pinned to an older release).
- val outdatedBefore = tool.listOutdated().getOrThrow()
+ // 3. list(outdated) should report it with a newer latestVersion (we pinned to an older release).
+ val outdatedBefore = tool.list(outdated = true, showPaths = true).getOrThrow()
val outdatedEntry = outdatedBefore.firstOrNull { it.name == pkg.name }
assertNotNull(outdatedEntry) {
- "expected ${pkg.name} in listOutdated() before upgrade, got $outdatedBefore"
+ "expected ${pkg.name} in list(outdated = true) before upgrade, got $outdatedBefore"
}
- assertEquals(pkg.version, outdatedEntry!!.currentVersion)
+ assertEquals(pkg.version, outdatedEntry!!.version)
assertNotEquals(pkg.version, outdatedEntry.latestVersion) {
"latestVersion must differ from the pinned ${pkg.version} for the outdated signal to mean anything"
}
@@ -166,7 +166,7 @@ class UvCliTest {
// This is exactly the production path that surfaces "{tool} is already up to date" in
// the External Tools settings balloon.
tool.upgrade(pkg.name).getOrThrow()
- val outdatedAfterUpgrade = tool.listOutdated().getOrThrow()
+ val outdatedAfterUpgrade = tool.list(outdated = true, showPaths = true).getOrThrow()
assertTrue(outdatedAfterUpgrade.any { it.name == pkg.name }) {
"uv tool upgrade respects the original pin; ${pkg.name} should still be outdated, got $outdatedAfterUpgrade"
}
@@ -174,9 +174,9 @@ class UvCliTest {
// 5. `install(name, reinstall = true)` (uv's `--reinstall`) drops the prior pin and
// installs the latest release. After that the outdated list must no longer mention it.
tool.install(pkg.name, reinstall = true).getOrThrow()
- val outdatedAfterReinstall = tool.listOutdated().getOrThrow()
+ val outdatedAfterReinstall = tool.list(outdated = true, showPaths = true).getOrThrow()
assertTrue(outdatedAfterReinstall.none { it.name == pkg.name }) {
- "after install(reinstall=true) ${pkg.name} should drop off listOutdated(), got $outdatedAfterReinstall"
+ "after install(reinstall=true) ${pkg.name} should drop off list(outdated = true), got $outdatedAfterReinstall"
}
}
}
diff --git a/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt b/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt
index 2824c4e14341..e2b658717c0b 100644
--- a/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt
+++ b/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt
@@ -23,18 +23,23 @@ import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.use
import com.intellij.openapi.vfs.newvfs.RefreshQueue
+import com.intellij.platform.eel.provider.getEelDescriptor
+import com.intellij.platform.eel.provider.toEelApi
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.psi.PsiFile
-import com.intellij.python.community.common.tools.ToolId
import com.intellij.python.pyproject.model.api.ModuleCreateInfo
import com.intellij.python.pyproject.model.api.autoConfigureSdkIfNeeded
import com.intellij.python.pyproject.model.api.getModuleInfo
import com.intellij.python.pyproject.statistics.PyProjectTomlCollector
+import com.intellij.python.pytools.PyTool
+import com.intellij.python.pytools.performToolInstallation
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.DropDownLink
import com.intellij.util.PlatformUtils
import com.jetbrains.python.PyBundle
import com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable
+import com.jetbrains.python.errorProcessing.ErrorSink
+import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.inspections.InspectionRunnerResult
import com.jetbrains.python.orLogException
import com.jetbrains.python.sdk.ModuleOrProject
@@ -190,14 +195,19 @@ private class UseProvidedInterpreterFix(private val myCreateSdkInfo: CreateSdkIn
private class SuggestToolInstallationFix(
private val myModule: Module,
private val myCreateSdkInfo: CreateSdkInfo.WillInstallTool,
- private val myTool: ToolId,
) : InterpreterFix {
override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink {
return ActionLink(myCreateSdkInfo.intentionName) {
+ val pyTool = PyTool.findByPackageName(myCreateSdkInfo.toolToInstall) ?: return@ActionLink
executor.execute {
- val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myTool.id)
+ val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myCreateSdkInfo.toolToInstall)
withBackgroundProgress(project, myCreateSdkInfo.intentionName, false) {
- lifetime.use { PyProjectSdkConfiguration.installToolAndShowErrorIfNeeded(myModule, myCreateSdkInfo.pathPersister, myCreateSdkInfo.toolToInstall) }
+ lifetime.use {
+ val eel = project.getEelDescriptor().toEelApi()
+ pyTool.performToolInstallation(eel).mapSuccess(myCreateSdkInfo.pathPersister).errorOrNull?.also {
+ ErrorSink().emit(it, project)
+ }
+ }
}
}
}
@@ -219,7 +229,7 @@ private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): Fin
}
is CreateSdkInfo.WillInstallTool -> {
logger.trace { "$this: Tool installation will be suggested to the user" }
- FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo, i.toolId))
+ FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo))
}
}
}
diff --git a/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt b/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt
index 5818fd16974a..acf307bfdf2a 100644
--- a/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt
@@ -12,6 +12,7 @@ import com.intellij.openapi.vfs.findPsiFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.python.community.common.tools.ToolId
+import com.intellij.python.community.impl.poetry.backend.PoetryPyTool
import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID
import com.intellij.python.community.impl.poetry.common.poetryPath
import com.intellij.python.community.services.systemPython.SystemPythonService
@@ -100,11 +101,11 @@ internal class PyPoetrySdkConfiguration : PyProjectTomlConfigurationExtension {
*/
else if (poetryLockExists || (isPoetryProject && checkToml)) {
val pathPersister: (Path) -> Unit = { path -> PropertiesComponent.getInstance().poetryPath = path.toString() }
- val toolName = "poetry"
+ val tool = PoetryPyTool.getInstance()
EnvCheckerResult.SuggestToolInstallation(
- toolToInstall = toolName,
+ toolToInstall = tool.packageName.name,
pathPersister = pathPersister,
- intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title.using.pip", "poetry")
+ intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title", tool.presentableName)
)
}
else EnvCheckerResult.CannotConfigure
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt
index 8499fae818d4..16f9031e67bb 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt
@@ -3,9 +3,14 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
+import com.intellij.platform.eel.provider.getEelDescriptor
+import com.intellij.platform.eel.provider.localEel
+import com.intellij.platform.eel.provider.toEelApi
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.Version
+import com.intellij.python.pytools.performToolInstallation
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.concurrency.annotations.RequiresEdt
@@ -18,7 +23,6 @@ import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
-import com.jetbrains.python.sdk.installExecutableViaPythonScript
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
@@ -27,9 +31,7 @@ import kotlinx.coroutines.flow.first
import org.jetbrains.annotations.ApiStatus.Internal
import java.nio.file.Path
-@Internal
internal abstract class CustomNewEnvironmentCreator(
- private val name: String,
model: PythonMutableTargetAddInterpreterModel
,
protected val errorSink: ErrorSink,
) : PythonNewEnvironmentCreator
(model) {
@@ -50,8 +52,8 @@ internal abstract class CustomNewEnvironmentCreator
(
fileSystem = model.fileSystem,
pathValidator = toolValidator,
validationRequestor = validationRequestor,
- labelText = message("sdk.create.custom.venv.executable.path", name),
- missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
+ labelText = message("sdk.create.custom.venv.executable.path", pyTool.presentableName),
+ missingExecutableText = message("sdk.create.custom.venv.missing.text", pyTool.presentableName),
installAction = createInstallFix(errorSink),
)
@@ -112,7 +114,7 @@ internal abstract class CustomNewEnvironmentCreator
(
*/
@RequiresEdt
protected fun createInstallFix(errorSink: ErrorSink): ActionLink {
- return ActionLink(message("sdk.create.custom.venv.install.fix.title.using.pip", name)) {
+ return ActionLink(message("sdk.create.custom.venv.install.fix.title", pyTool.presentableName)) {
PythonSdkFlavor.clearExecutablesCache()
installExecutable(errorSink)
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
@@ -122,55 +124,27 @@ internal abstract class CustomNewEnvironmentCreator
(
}
/**
- * Downloads the selected downloadable env (if selected), then installs the necessary executable in the Python environment.
- *
- * Initiates a blocking modal progress task to:
- * 1. Ensure that the environment is downloaded (if selected).
- * 2. Ensure that pip is installed.
- * 3. Install the executable (specified by `name`) using either a custom installation script or via pip.
+ * Installs the [pyTool] executable behind a single modal progress via its `performToolInstallation`
+ * extension (prefers `uv tool install`, falls back to a pip install into a system Python). On
+ * success the resolved launcher is persisted.
*/
@RequiresEdt
private fun installExecutable(errorSink: ErrorSink) {
- val baseInterpreter = model.state.baseInterpreter.get()
-
- val installedSdk = when (baseInterpreter) {
- is InstallableSelectableInterpreter -> installBaseSdk(baseInterpreter.installableSdk)
- ?.let {
- val sdkWrapper =
- runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
- model.fileSystem.wrapSdk(it)
- }
- val installed = model.addInstalledInterpreter(sdkWrapper.homePath, baseInterpreter.pythonInfo)
- model.state.baseInterpreter.set(installed)
- installed
- }
- is DetectedSelectableInterpreter, is ExistingSelectableInterpreter, is ManuallyAddedSelectableInterpreter, null -> null
- }
-
- // installedSdk is null when the selected sdk isn't downloadable
- // model.state.baseInterpreter could be null if no SDK was selected
- val pythonExecutablePath = installedSdk?.homePath ?: model.state.baseInterpreter.get()?.homePath
- val pythonExecutable = pythonExecutablePath?.let { model.fileSystem.getBinaryToExec(it) } ?: return
-
- runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title.using.pip", name)) {
- val versionArgs: List = installationVersion?.let { listOf("-v", it) } ?: emptyList()
- when (val r = installExecutableViaPythonScript(pythonExecutable, "-n", name, *versionArgs.toTypedArray())) {
- is Result.Success -> {
- val pathHolder = PathHolder.Eel(r.result)
- savePathToExecutableToProperties(pathHolder as? P)
- }
- is Result.Failure -> {
- errorSink.emit(r.error)
- }
+ runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title", pyTool.presentableName)) {
+ val eel = model.projectPathFlows.projectPath.first()?.getEelDescriptor()?.toEelApi() ?: localEel
+ when (val r = pyTool.performToolInstallation(eel)) {
+ is Result.Success -> savePathToExecutableToProperties(PathHolder.Eel(r.result) as? P)
+ is Result.Failure -> errorSink.emit(r.error)
}
}
}
internal abstract val interpreterType: InterpreterType
- internal abstract val toolValidator: ToolValidator
+ /** The tool this creator installs; drives [installExecutable] via [performToolInstallation]. */
+ internal abstract val pyTool: PyTool
- internal open val installationVersion: String? = null
+ internal abstract val toolValidator: ToolValidator
protected abstract suspend fun setupEnvSdk(moduleBasePath: Path): PyResult
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 c7b3f1af46b0..23a98263bf3e 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt
@@ -19,7 +19,7 @@ import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.python.community.common.tools.ToolId
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
-import com.intellij.python.community.impl.pipenv.PIPENV_ICON
+import com.intellij.python.community.impl.pipenv.icons.PythonCommunityImplPipenvIcons
import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
import com.intellij.python.hatch.icons.PythonHatchIcons
@@ -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 = false),
- PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PIPENV_ICON, sshAutoUploadRequired = false),
+ PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PythonCommunityImplPipenvIcons.PythonClosed, sshAutoUploadRequired = false),
UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonUvCommonIcons.UV, sshAutoUploadRequired = true, { true }),
HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, sshAutoUploadRequired = false),
PYTHON(VENV_TOOL_ID, "sdk.create.custom.python", PythonParserIcons.PythonFile, sshAutoUploadRequired = false, { true })
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt
index 874680600ad2..83243dd52c69 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchNewEnvironmentCreator.kt
@@ -10,6 +10,8 @@ import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.util.progress.withProgressText
import com.intellij.python.hatch.HatchConfiguration
+import com.intellij.python.hatch.HatchPyTool
+import com.intellij.python.pytools.PyTool
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.getHatchService
import com.intellij.ui.dsl.builder.Panel
@@ -35,8 +37,9 @@ import java.nio.file.Path
internal class HatchNewEnvironmentCreator(
override val model: PythonMutableTargetAddInterpreterModel
,
errorSink: ErrorSink,
-) : CustomNewEnvironmentCreator
("hatch", model, errorSink) {
+) : CustomNewEnvironmentCreator
(model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.HATCH
+ override val pyTool: PyTool = HatchPyTool.getInstance()
override val toolValidator: ToolValidator
= model.hatchViewModel.toolValidator
private lateinit var hatchFormFields: HatchFormFields
override val toolExecutable: ObservableProperty?> = model.hatchViewModel.hatchExecutable
@@ -87,7 +90,7 @@ internal class HatchNewEnvironmentCreator(
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
is PathHolder.Eel -> basePythonBinaryPath.path
- else -> return PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
+ else -> return PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath))
}
val hatchExecutablePath = when (val hatchBinary = model.hatchViewModel.hatchExecutable.get()?.pathHolder) {
is PathHolder.Eel -> hatchBinary.path
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 8fcd4c21f1db..1d72198b364c 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
@@ -4,7 +4,9 @@ package com.jetbrains.python.sdk.add.v2.pipenv
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.projectRoots.Sdk
+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
@@ -21,8 +23,9 @@ import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkWithProgressReport
import com.jetbrains.python.statistics.InterpreterType
import java.nio.file.Path
-internal class EnvironmentCreatorPip
(model: PythonMutableTargetAddInterpreterModel
, errorSink: ErrorSink) : CustomNewEnvironmentCreator
("pipenv", model, errorSink) {
+internal class EnvironmentCreatorPip
(model: PythonMutableTargetAddInterpreterModel
, errorSink: ErrorSink) : CustomNewEnvironmentCreator
(model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.PIPENV
+ override val pyTool: PyTool = PipEnvPyTool.getInstance()
override val toolValidator: ToolValidator
= model.pipenvViewModel.toolValidator
override val toolExecutable: ObservableProperty?> = model.pipenvViewModel.pipenvExecutable
override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder ->
@@ -40,7 +43,7 @@ internal class EnvironmentCreatorPip(model: PythonMutableTargetA
installPackages = false
)
}
- else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
+ else -> PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath))
}
}
}
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt
index 7a9f4f43cc83..542ae4006227 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt
@@ -14,7 +14,9 @@ import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.python.community.impl.poetry.backend.PoetryPyTool
import com.intellij.python.community.impl.poetry.common.poetryPath
+import com.intellij.python.pytools.PyTool
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.platform.util.progress.withProgressText
@@ -54,10 +56,10 @@ internal class EnvironmentCreatorPoetry
(
model: PythonMutableTargetAddInterpreterModel
,
private val module: Module?,
errorSink: ErrorSink,
-) : CustomNewEnvironmentCreator
("poetry", model, errorSink) {
+) : CustomNewEnvironmentCreator
(model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.POETRY
+ override val pyTool: PyTool = PoetryPyTool.getInstance()
override val toolValidator: ToolValidator
= model.poetryViewModel.toolValidator
- override val installationVersion: String = "1.8.0"
override val toolExecutable: ObservableProperty?> = model.poetryViewModel.poetryExecutable
override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder ->
savePathForEelOnly(pathHolder) { path -> PropertiesComponent.getInstance().poetryPath = path.toString() }
@@ -129,7 +131,7 @@ internal class EnvironmentCreatorPoetry(
inProjectEnv = isInProjectEnvFlow.value,
)
}
- else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
+ else -> PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath))
}
}
@@ -147,7 +149,7 @@ internal class EnvironmentCreatorPoetry
(
private fun addInProjectCheckbox(panel: Panel) {
with(panel) {
row("") {
- checkBox(PyBundle.message("python.sdk.poetry.dialog.add.new.environment.in.project.checkbox"))
+ checkBox(message("python.sdk.poetry.dialog.add.new.environment.in.project.checkbox"))
.bindSelected(isInProjectEnvProp)
}
}
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt
index 0ff29f9c0461..40c6b3f94e9e 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt
@@ -11,6 +11,8 @@ import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.pyproject.PyProjectToml
+import com.intellij.python.uv.backend.UvPyTool
+import com.intellij.python.pytools.PyTool
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
@@ -70,8 +72,9 @@ internal class EnvironmentCreatorUv
(
model: PythonMutableTargetAddInterpreterModel
,
private val module: Module?,
errorSink: ErrorSink,
-) : CustomNewEnvironmentCreator
("uv", model, errorSink) {
+) : CustomNewEnvironmentCreator
(model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.UV
+ override val pyTool: PyTool = UvPyTool.getInstance()
override val toolValidator: ToolValidator
= model.uvViewModel.toolValidator
private val executableFlow = MutableStateFlow(model.uvViewModel.uvExecutable.get())
private val pythonVersion: ObservableMutableProperty = propertyGraph.property(null)
diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
index 3fc613e710df..6968a7c36215 100644
--- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
@@ -6,35 +6,18 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.util.Disposer
import com.intellij.python.community.common.tools.ToolId
-import com.intellij.python.community.services.systemPython.SystemPythonService
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.python.pyproject.model.api.suggestSdk
-import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonPluginDisposable
-import com.jetbrains.python.errorProcessing.PyResult
+import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
import com.jetbrains.python.sdk.configurePythonSdk
-import com.jetbrains.python.sdk.installExecutableViaPythonScript
-import com.jetbrains.python.errorProcessing.ErrorSink
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import java.nio.file.Path
object PyProjectSdkConfiguration {
- internal suspend fun installToolAndShowErrorIfNeeded(module: Module, pathPersister: (Path) -> Unit, toolToInstall: String) {
- performToolInstallation(pathPersister, toolToInstall).errorOrNull?.also {
- ErrorSink().emit(it, module.project)
- }
- }
-
- private suspend fun performToolInstallation(pathPersister: (Path) -> Unit, toolToInstall: String): PyResult {
- val systemPython = SystemPythonService().findSystemPythons().firstOrNull()
- ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.python"))
- return installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", toolToInstall).mapSuccess(pathPersister)
- }
-
suspend fun setSdkUsingCreateSdkInfo(
module: Module, createSdkInfoWithTool: CreateSdkInfoWithTool,
): Boolean = withContext(Dispatchers.Default) {
diff --git a/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt b/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt
new file mode 100644
index 000000000000..e48465eecc0f
--- /dev/null
+++ b/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt
@@ -0,0 +1,85 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.sdk.configuration
+
+import com.intellij.platform.eel.EelApi
+import com.intellij.python.community.execService.BinOnEel
+import com.intellij.python.community.services.systemPython.SystemPython
+import com.intellij.python.community.services.systemPython.SystemPythonService
+import com.intellij.python.pytools.InstalledInfo
+import com.intellij.python.pytools.PyTool
+import com.intellij.python.pytools.PyToolManager
+import com.intellij.python.pytools.PyToolManagerProvider
+import com.intellij.python.pytools.configuration.ConfigurablePyTool
+import com.intellij.python.pytools.getToolVersion
+import com.jetbrains.python.Result
+import com.jetbrains.python.errorProcessing.PyResult
+import com.jetbrains.python.getOrNull
+import com.jetbrains.python.packaging.PyPackageVersionNormalizer
+import com.jetbrains.python.packaging.repository.PyPiPackageRepository
+import com.jetbrains.python.sdk.add.v2.FileSystem
+import com.jetbrains.python.sdk.add.v2.PathHolder
+import com.jetbrains.python.sdk.add.v2.toFileSystem
+import com.jetbrains.python.sdk.impl.PySdkBundle
+import com.jetbrains.python.sdk.installExecutableViaPythonScript
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+import java.nio.file.Path
+
+/**
+ * Terminal [PyToolManagerProvider] fallback: yields a manager that pip-installs into the first system
+ * Python of the target environment. Registered last, so it is used only when no higher-priority
+ * provider (e.g. uv) can operate there.
+ */
+@ApiStatus.Internal
+class SystemPythonToolManagerProvider : PyToolManagerProvider {
+ override suspend fun forEel(eel: EelApi): PyToolManager? {
+ val systemPython = SystemPythonService().findSystemPythons(eelApi = eel).firstOrNull() ?: return null
+ return SystemPythonToolManager(eel.toFileSystem(), systemPython)
+ }
+}
+
+/** pip-installs tools into [systemPython] via the `pycharm_package_installer.py` helper. */
+private class SystemPythonToolManager(
+ private val fileSystem: FileSystem,
+ private val systemPython: SystemPython,
+) : PyToolManager {
+ override suspend fun install(tool: PyTool): PyResult {
+ installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", tool.packageName.name).getOr { return it }
+ val executable = fileSystem.detectTool(tool.packageName.name)
+ ?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", tool.packageName.name, fileSystem.userReadableName))
+ return Result.success(executable.path)
+ }
+
+ /** The pip helper always installs the latest release, so an upgrade is just a fresh install. */
+ override suspend fun upgrade(tool: PyTool): PyResult = install(tool)
+
+ /**
+ * Every configurable tool that is actually installed (resolved on [fileSystem]), with its `--version`
+ * probed and the latest release looked up from PyPI. When PyPI is unreachable the latest version falls
+ * back to the installed one (i.e. reported as up to date).
+ */
+ override suspend fun list(): Map {
+ return PyTool.EP_NAME.extensionList.filter { it is ConfigurablePyTool }.mapNotNull { tool ->
+ val name = tool.packageName.name
+ val executable = fileSystem.detectTool(name) ?: return@mapNotNull null
+ val installed = BinOnEel(executable.path).getToolVersion(name).getOrNull()?.value ?: return@mapNotNull null
+ val latest = latestPyPiVersion(name) ?: installed
+ tool to InstalledInfo(path = executable.path, installedVersion = installed, latestVersion = latest)
+ }.toMap()
+ }
+
+ /**
+ * Latest stable release of [packageName] from PyPI, or `null` if it can't be determined. Queried
+ * app-level through [PyPiPackageRepository] (no project needed); `availableVersions` come back sorted
+ * newest-first, and we skip pre-/dev-releases to match the default "stable only" upgrade policy.
+ */
+ private suspend fun latestPyPiVersion(packageName: String): String? {
+ val details = withContext(Dispatchers.IO) { PyPiPackageRepository.buildPackageDetails(packageName) }.getOrNull()
+ ?: return null
+ return details.availableVersions.firstOrNull { version ->
+ val normalized = PyPackageVersionNormalizer.normalize(version)
+ normalized == null || (normalized.pre == null && normalized.dev == null)
+ }
+ }
+}
diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt
index 8c3e50836b03..cd9af77d000d 100644
--- a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt
+++ b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkFlavor.kt
@@ -1,7 +1,7 @@
// 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.pipenv
-import com.intellij.python.community.impl.pipenv.PIPENV_ICON
+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
@@ -10,7 +10,7 @@ import javax.swing.Icon
internal object PyPipEnvSdkFlavor : CPythonSdkFlavor() {
- override fun getIcon(): Icon = PIPENV_ICON
+ override fun getIcon(): Icon = PipEnvPyTool.getInstance().icon
override fun getFlavorDataClass(): Class = PyFlavorData.Empty::class.java
override fun isValidSdkPath(pythonBinaryPath: Path): Boolean = false
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
index 1039e7397331..db527c516802 100644
--- a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
@@ -42,7 +42,7 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
EnvCheckerResult.SuggestToolInstallation(
toolToInstall = toolName,
pathPersister = pathPersister,
- intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title.using.pip", toolName)
+ intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title", toolName)
)
} else baseCheckResult
}