mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[python][sdk] check poetry.lock in poetry auto configurator (PY-82840)
+ refactor PyProjectSdkConfiguration to return PyResult for SDK creation methods: In case of creation failure, the actual error was hidden and some random sdk was assigned instead of the real one. For example, if we open a poetry project and project and poetry sdk creation fails - any random sdk was assigned to the project (last used hatch or conda for example). Now ShowMessageErrorSink is used. GitOrigin-RevId: 015fc469b6b5d934b49278ccfb3afc683b61554e
This commit is contained in:
committed by
intellij-monorepo-bot
parent
24a13684c0
commit
9114137195
@@ -51,7 +51,7 @@ sdk.create.pipenv.suggestion=Create a pipenv environment using {0}
|
||||
sdk.create.pipenv.permission=File Pipfile contains project dependencies. Would you like to create a pipenv environment using it?
|
||||
sdk.create.pipenv.exception.dialog.title=Failed To Create Pipenv Environment
|
||||
|
||||
sdk.set.up.poetry.environment=Set up a poetry environment using {0}
|
||||
sdk.set.up.poetry.environment=Set up Poetry environment
|
||||
sdk.progress.text.setting.up.poetry.environment=Setting up poetry environment
|
||||
sdk.dialog.title.failed.to.set.up.poetry.environment=Failed To Set Up Poetry Environment
|
||||
sdk.dialog.title.setting.up.poetry.environment=Setting Up Poetry Environment
|
||||
|
||||
+19
-12
@@ -16,7 +16,9 @@ import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationC
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewCondaEnvFromFilePanel
|
||||
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
@@ -43,29 +45,34 @@ import java.nio.file.Path
|
||||
* TODO: Support remote target (ie \\wsl)
|
||||
*/
|
||||
internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR)
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.CONFIGURATOR)
|
||||
|
||||
override suspend fun getIntention(module: Module): @IntentionName String? = getEnvironmentYml(module)?.let {
|
||||
PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion")
|
||||
override suspend fun getIntention(module: Module): @IntentionName String? {
|
||||
val isReadyToSetup = withContext(Dispatchers.IO) {
|
||||
getEnvironmentYml(module) != null &&
|
||||
suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) } != null
|
||||
}
|
||||
|
||||
return if (isReadyToSetup) PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion") else null
|
||||
}
|
||||
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.INSPECTION)
|
||||
|
||||
private fun getEnvironmentYml(module: Module) = listOf(
|
||||
CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME,
|
||||
CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME,
|
||||
).firstNotNullOfOrNull { findAmongRoots(module, it) }
|
||||
|
||||
private suspend fun createAndAddSdk(module: Module, source: Source): Sdk? {
|
||||
private suspend fun createAndAddSdk(module: Module, source: Source): PyResult<Sdk?> {
|
||||
val targetConfig = PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(module)
|
||||
if (targetConfig != null) {
|
||||
// Remote targets aren't supported yet
|
||||
return null
|
||||
return PyResult.success(null)
|
||||
}
|
||||
|
||||
val (condaExecutable, environmentYml) = askForEnvData(module, source) ?: return null
|
||||
return createAndAddCondaEnv(module, condaExecutable, environmentYml)?.also {
|
||||
PythonSdkUpdater.scheduleUpdate(it, module.project)
|
||||
val (condaExecutable, environmentYml) = askForEnvData(module, source) ?: return PyResult.success(null)
|
||||
return createAndAddCondaEnv(module, condaExecutable, environmentYml).onSuccess { sdk ->
|
||||
sdk?.let { PythonSdkUpdater.scheduleUpdate(it, module.project) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +102,10 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
|
||||
if (permitted) envData else null
|
||||
}
|
||||
|
||||
private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): Sdk? {
|
||||
private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): PyResult<Sdk?> {
|
||||
thisLogger().debug("Creating conda environment")
|
||||
|
||||
val sdk = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return null
|
||||
val sdk = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return PyResult.success(null)
|
||||
PySdkConfigurationCollector.logCondaEnv(module.project, CondaEnvResult.CREATED)
|
||||
|
||||
val shared = PyCondaSdkCustomizer.instance.sharedEnvironmentsByDefault
|
||||
@@ -113,7 +120,7 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
|
||||
SdkConfigurationUtil.addSdk(sdk)
|
||||
}
|
||||
|
||||
return sdk
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
|
||||
private fun executableToEventField(condaExecutable: String?): InputData {
|
||||
|
||||
+7
-8
@@ -10,9 +10,9 @@ import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBund
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.cli.HatchEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.hatch.sdk.createSdk
|
||||
import com.jetbrains.python.orLogException
|
||||
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
|
||||
import com.jetbrains.python.util.runWithModalBlockingOrInBackground
|
||||
|
||||
@@ -35,21 +35,20 @@ internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
return intention
|
||||
}
|
||||
|
||||
private fun createSdk(module: Module): Sdk? = runWithModalBlockingOrInBackground(
|
||||
private fun createSdk(module: Module): PyResult<Sdk> = runWithModalBlockingOrInBackground(
|
||||
project = module.project,
|
||||
msg = PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment")
|
||||
) {
|
||||
val hatchService = module.getHatchService().orLogException(LOGGER)
|
||||
val createdEnvironment = hatchService?.createVirtualEnvironment()?.orLogException(LOGGER)
|
||||
?: return@runWithModalBlockingOrInBackground null
|
||||
val hatchService = module.getHatchService().getOr { return@runWithModalBlockingOrInBackground it }
|
||||
val createdEnvironment = hatchService.createVirtualEnvironment().getOr { return@runWithModalBlockingOrInBackground it }
|
||||
|
||||
val hatchVenv = HatchVirtualEnvironment(HatchEnvironment.DEFAULT, createdEnvironment)
|
||||
val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module).orLogException(LOGGER)
|
||||
val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module)
|
||||
sdk
|
||||
}
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createSdk(module)
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = createSdk(module)
|
||||
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module)
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createSdk(module)
|
||||
|
||||
override fun supportsHeadlessModel(): Boolean = true
|
||||
}
|
||||
+5
-5
@@ -44,16 +44,16 @@ private val LOGGER = Logger.getInstance(PyPipfileSdkConfiguration::class.java)
|
||||
|
||||
internal class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, Source.CONFIGURATOR)
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?> = createAndAddSDk(module, Source.CONFIGURATOR)
|
||||
|
||||
override suspend fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PIP_FILE)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) }
|
||||
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, Source.INSPECTION)
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?> = createAndAddSDk(module, Source.INSPECTION)
|
||||
|
||||
private suspend fun createAndAddSDk(module: Module, source: Source): Sdk? {
|
||||
val pipEnvExecutable = askForEnvData(module, source) ?: return null
|
||||
private suspend fun createAndAddSDk(module: Module, source: Source): PyResult<Sdk?> {
|
||||
val pipEnvExecutable = askForEnvData(module, source) ?: return PyResult.success(null)
|
||||
PropertiesComponent.getInstance().pipEnvPath = pipEnvExecutable.pipEnvPath.pathString
|
||||
return createPipEnv(module).getOrLogException(LOGGER)
|
||||
return createPipEnv(module)
|
||||
}
|
||||
|
||||
private suspend fun askForEnvData(module: Module, source: Source): PyAddNewPipEnvFromFilePanel.Data? {
|
||||
|
||||
+13
-90
@@ -1,42 +1,31 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.pycharm.community.ide.impl.configuration
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.util.progress.reportRawProgress
|
||||
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
|
||||
import com.intellij.python.community.impl.poetry.poetryPath
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.ui.IdeBorderFactory
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.poetry.findPoetryLock
|
||||
import com.jetbrains.python.poetry.getPyProjectTomlForPoetry
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
|
||||
import com.jetbrains.python.sdk.poetry.*
|
||||
import com.jetbrains.python.sdk.poetry.ui.PyAddNewPoetryFromFilePanel
|
||||
import com.jetbrains.python.sdk.setAssociationToModule
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.BorderLayout
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
@@ -47,77 +36,43 @@ internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
@NlsSafe
|
||||
override suspend fun getIntention(module: Module): String? = reportRawProgress {
|
||||
it.text(PyBundle.message("python.sdk.validating.environment"))
|
||||
val toml = PyProjectToml.findFile(module)
|
||||
if (toml == null) {
|
||||
return@reportRawProgress null
|
||||
|
||||
val isPoetryProject = withContext(Dispatchers.IO) {
|
||||
PyProjectToml.findFile(module)?.let { toml -> getPyProjectTomlForPoetry(toml) } != null ||
|
||||
findPoetryLock(module) != null
|
||||
}
|
||||
|
||||
val isPoetry = getPyProjectTomlForPoetry(toml) != null
|
||||
if (!isPoetry) {
|
||||
return@reportRawProgress null
|
||||
}
|
||||
val isReadyToSetup = isPoetryProject && getPoetryExecutable().successOrNull != null
|
||||
|
||||
return@reportRawProgress PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment", toml.name)
|
||||
return if (isReadyToSetup) PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment") else null
|
||||
}
|
||||
|
||||
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, false)
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = createPoetry(module)
|
||||
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, true)
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createPoetry(module)
|
||||
|
||||
override fun supportsHeadlessModel(): Boolean = true
|
||||
|
||||
private suspend fun createAndAddSDk(module: Module, inspection: Boolean): Sdk? {
|
||||
val poetryEnvExecutable = askForEnvData(module, inspection) ?: return null
|
||||
PropertiesComponent.getInstance().poetryPath = poetryEnvExecutable.poetryPath.pathString
|
||||
return createPoetry(module).getOrLogException(LOGGER)
|
||||
}
|
||||
|
||||
private suspend fun askForEnvData(module: Module, inspection: Boolean): PyAddNewPoetryFromFilePanel.Data? {
|
||||
val poetryExecutable = getPoetryExecutable().getOrLogException(LOGGER)
|
||||
val isHeadlessEnv = ApplicationManager.getApplication().isHeadlessEnvironment
|
||||
|
||||
if ((inspection || isHeadlessEnv) && validatePoetryExecutable(poetryExecutable) == null) {
|
||||
return PyAddNewPoetryFromFilePanel.Data(poetryExecutable!!)
|
||||
}
|
||||
else if (isHeadlessEnv) {
|
||||
return null
|
||||
}
|
||||
|
||||
var permitted = false
|
||||
var envData: PyAddNewPoetryFromFilePanel.Data? = null
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
val dialog = Dialog(module)
|
||||
|
||||
permitted = dialog.showAndGet()
|
||||
envData = dialog.envData
|
||||
|
||||
LOGGER.debug("Dialog exit code: ${dialog.exitCode}, $permitted")
|
||||
}
|
||||
|
||||
return if (permitted) envData else null
|
||||
}
|
||||
|
||||
private suspend fun createPoetry(module: Module): PyResult<Sdk> =
|
||||
withBackgroundProgress(module.project, PyCharmCommunityCustomizationBundle.message("sdk.progress.text.setting.up.poetry.environment")) {
|
||||
LOGGER.debug("Creating poetry environment")
|
||||
|
||||
val basePath = module.basePath?.let { Path.of(it) }
|
||||
if (basePath == null) {
|
||||
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid",module.basePath))
|
||||
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", module.basePath))
|
||||
}
|
||||
val tomlFile = PyProjectToml.findFile(module)
|
||||
val poetry = setupPoetry(basePath, null, true, tomlFile == null).getOr { return@withBackgroundProgress it }
|
||||
|
||||
val path = withContext(Dispatchers.IO) { VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(poetry)) }
|
||||
if (path == null) {
|
||||
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable","python", poetry))
|
||||
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", poetry))
|
||||
}
|
||||
|
||||
val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.pathString)
|
||||
if (file == null) {
|
||||
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable","python", path))
|
||||
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", path))
|
||||
}
|
||||
|
||||
LOGGER.debug("Setting up associated poetry environment: $path, $basePath")
|
||||
@@ -137,36 +92,4 @@ internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
|
||||
PyResult.success(sdk)
|
||||
}
|
||||
|
||||
|
||||
private class Dialog(module: Module) : DialogWrapper(module.project, false, IdeModalityType.IDE) {
|
||||
|
||||
private val panel = PyAddNewPoetryFromFilePanel(module)
|
||||
|
||||
val envData
|
||||
get() = panel.envData
|
||||
|
||||
init {
|
||||
title = PyCharmCommunityCustomizationBundle.message("sdk.dialog.title.setting.up.poetry.environment")
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return JPanel(BorderLayout()).apply {
|
||||
val border = IdeBorderFactory.createEmptyBorder(JBUI.insets(4, 0, 6, 0))
|
||||
val message = PyCharmCommunityCustomizationBundle.message("sdk.notification.label.set.up.poetry.environment.from.pyproject.toml.dependencies")
|
||||
|
||||
add(
|
||||
JBUI.Panels.simplePanel(JBLabel(message)).withBorder(border),
|
||||
BorderLayout.NORTH
|
||||
)
|
||||
|
||||
add(panel, BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun postponeValidation(): Boolean = false
|
||||
|
||||
override fun doValidateAll(): List<ValidationInfo> = panel.validateAll()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-13
@@ -9,7 +9,6 @@ import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.fileTypes.FileTypeRegistry
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
@@ -30,6 +29,7 @@ import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PySdkBundle
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.packaging.PyPackageManager
|
||||
import com.jetbrains.python.packaging.PyPackageUtil
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
@@ -41,8 +41,6 @@ import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
|
||||
import com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously
|
||||
import com.jetbrains.python.sdk.isTargetBased
|
||||
import com.jetbrains.python.sdk.showSdkExecutionException
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.BorderLayout
|
||||
@@ -54,17 +52,17 @@ import kotlin.io.path.Path
|
||||
private val LOGGER = fileLogger()
|
||||
|
||||
class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR)
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.CONFIGURATOR)
|
||||
|
||||
override suspend fun getIntention(module: Module): @IntentionName String? =
|
||||
getRequirementsTxtOrSetupPy(module)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.venv.suggestion", it.name) }
|
||||
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.INSPECTION)
|
||||
|
||||
private suspend fun createAndAddSdk(module: Module, source: Source): Sdk? {
|
||||
private suspend fun createAndAddSdk(module: Module, source: Source): PyResult<Sdk?> {
|
||||
val existingSdks = PythonSdkUtil.getAllSdks()
|
||||
|
||||
val data = askForEnvData(module, existingSdks, source) ?: return null
|
||||
val data = askForEnvData(module, existingSdks, source) ?: return PyResult.success(null)
|
||||
|
||||
val (location, chosenBaseSdk, requirementsTxtOrSetupPy) = data
|
||||
val systemIndependentLocation = Path(location)
|
||||
@@ -87,7 +85,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte
|
||||
if (requirementsTxtOrSetupPyFile == null) {
|
||||
PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.DEPS_NOT_FOUND)
|
||||
thisLogger().warn("File with dependencies is not found: $requirementsTxtOrSetupPy")
|
||||
return sdk
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
|
||||
val isRequirements = requirementsTxtOrSetupPyFile.name != SetupPyManager.SETUP_PY
|
||||
@@ -100,8 +98,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte
|
||||
val pythonPackageManager = PythonPackageManager.forSdk(module.project, sdk)
|
||||
pythonPackageManager.sync().getOr {
|
||||
PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.INSTALLATION_FAILURE)
|
||||
ShowingMessageErrorSync.emit(it.error)
|
||||
return null
|
||||
return it
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -110,13 +107,12 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte
|
||||
}
|
||||
}
|
||||
|
||||
return sdk
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.INSTALLATION_FAILURE)
|
||||
showSdkExecutionException(chosenBaseSdk, e, PyBundle.message("python.packaging.failed.to.install.packages.title"))
|
||||
LOGGER.warn("Exception during creating virtual environment", e)
|
||||
return null
|
||||
return PyResult.localizedError(e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-18
@@ -3,7 +3,6 @@ package com.intellij.pycharm.community.ide.impl.configuration
|
||||
|
||||
import com.intellij.codeInspection.util.IntentionName
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
@@ -24,27 +23,15 @@ import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
companion object {
|
||||
private val LOGGER = Logger.getInstance(PyUvSdkConfiguration::class.java)
|
||||
}
|
||||
|
||||
override suspend fun getIntention(module: Module): @IntentionName String? {
|
||||
return PyProjectToml.findFile(module)?.let { toml ->
|
||||
getUvExecutable()?.let { PyCharmCommunityCustomizationBundle.message("sdk.set.up.uv.environment", toml.name) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createUv(module).getOr {
|
||||
LOGGER.warn(it.error.message)
|
||||
return null
|
||||
}
|
||||
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = createUv(module)
|
||||
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? {
|
||||
return createUv(module).getOr {
|
||||
LOGGER.warn(it.error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createUv(module)
|
||||
|
||||
override fun supportsHeadlessModel(): Boolean = true
|
||||
|
||||
@@ -62,13 +49,13 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
return PyResult.failure(MessageError("Can't determine working dir for the module"))
|
||||
}
|
||||
|
||||
val sdk = setupNewUvSdkAndEnv(workingDir, PythonSdkUtil.getAllSdks(), null)
|
||||
sdk.onSuccess {
|
||||
val sdkSetupResult = setupNewUvSdkAndEnv(workingDir, PythonSdkUtil.getAllSdks(), null)
|
||||
sdkSetupResult.onSuccess {
|
||||
withContext(Dispatchers.EDT) {
|
||||
SdkConfigurationUtil.addSdk(it)
|
||||
it.setAssociationToModule(sdkAssociatedModule)
|
||||
}
|
||||
}
|
||||
return sdk
|
||||
return sdkSetupResult
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
implementationClass="com.jetbrains.python.requirements.inspections.tools.OutdatedRequirementInspection"/>
|
||||
|
||||
<pluginSuggestionProvider order="first" implementation="com.jetbrains.python.suggestions.PycharmProSuggestionProvider"/>
|
||||
<postStartupActivity implementation="com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPostStartupActivity"/>
|
||||
<postStartupActivity implementation="com.jetbrains.python.poetry.PoetryPyProjectTomlPostStartupActivity"/>
|
||||
|
||||
<!--Poetry project model-->
|
||||
<projectOpenProcessor implementation="com.jetbrains.python.projectModel.poetry.PoetryProjectOpenProcessor"/>
|
||||
|
||||
@@ -4,8 +4,12 @@ import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.roots.ModuleRootManager
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.util.FileName
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import java.nio.file.Path
|
||||
|
||||
val Module.rootManager: ModuleRootManager
|
||||
get() = ModuleRootManager.getInstance(this)
|
||||
@@ -28,4 +32,9 @@ fun findAmongRoots(module: Module, fileName: String): VirtualFile? {
|
||||
if (file != null) return file
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Internal
|
||||
suspend fun Module.findAmongRoots(fileName: FileName): Path? = withContext(Dispatchers.IO) {
|
||||
findAmongRoots(this@findAmongRoots, fileName.value)?.toNioPath()
|
||||
}
|
||||
+3
-2
@@ -7,6 +7,7 @@ import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.progress.runBlockingMaybeCancellable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
@@ -36,7 +37,7 @@ interface PyProjectSdkConfigurationExtension {
|
||||
*
|
||||
* Rule of thumb is to explicitly ask a user if sdk creation is desired and allowed.
|
||||
*/
|
||||
suspend fun createAndAddSdkForConfigurator(module: Module): Sdk?
|
||||
suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?>
|
||||
|
||||
/**
|
||||
* An implementation is responsible for interpreter setup and registration in IDE.
|
||||
@@ -44,7 +45,7 @@ interface PyProjectSdkConfigurationExtension {
|
||||
*
|
||||
* You're free here to create sdk immediately, without any user permission since quick fix is explicitly clicked.
|
||||
*/
|
||||
suspend fun createAndAddSdkForInspection(module: Module): Sdk?
|
||||
suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?>
|
||||
|
||||
/**
|
||||
* Called by sdk configurator and interpreter inspection
|
||||
|
||||
+3
-64
@@ -1,31 +1,21 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.poetry
|
||||
// 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.poetry
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ProjectFileIndex
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.findDocument
|
||||
import com.intellij.openapi.vfs.findPsiFile
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.PsiElementFilter
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
|
||||
import com.jetbrains.python.poetry.VersionType.Companion.getVersionType
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.PythonSdkUpdater
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.findAmongRoots
|
||||
import com.jetbrains.python.sdk.poetry.VersionType.Companion.getVersionType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -41,9 +31,6 @@ import java.util.concurrent.ConcurrentMap
|
||||
* This source code is edited by @koxudaxi Koudai Aono <koxudaxi@gmail.com>
|
||||
*/
|
||||
|
||||
const val POETRY_LOCK: String = "poetry.lock"
|
||||
const val POETRY_TOML: String = "poetry.toml"
|
||||
|
||||
private val LOGGER = Logger.getInstance("#com.jetbrains.python.sdk.poetry")
|
||||
|
||||
suspend fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): VirtualFile? =
|
||||
@@ -59,54 +46,6 @@ suspend fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): VirtualFile? =
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun poetryToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) {
|
||||
findAmongRoots(module, POETRY_TOML)?.takeIf { readAction { ProjectFileIndex.getInstance(module.project).isInProject(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a post-startup activity for PyProjectToml files in a project.
|
||||
* It finds valid python versions in PyProjectToml files and saves them in PyProjectTomlPythonVersionsService.
|
||||
*/
|
||||
private class PoetryPyProjectTomlPostStartupActivity : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
val modulesRoots = PythonSdkUpdater.getModuleRoots(project)
|
||||
for (module in modulesRoots) {
|
||||
val tomlFile = withContext(Dispatchers.IO) {
|
||||
module.findChild(PY_PROJECT_TOML)?.let { getPyProjectTomlForPoetry(it) }
|
||||
} ?: continue
|
||||
val versionString = poetryFindPythonVersionFromToml(tomlFile, project) ?: continue
|
||||
|
||||
PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, versionString)
|
||||
addDocumentListener(tomlFile, project, module)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a document listener to a given toml file.
|
||||
* Updates PyProjectTomlPythonVersionsService map if needed.
|
||||
*
|
||||
* @param tomlFile The VirtualFile representing the toml file.
|
||||
* @param project The Project in which the toml file exists.
|
||||
* @param module The VirtualFile representing the module.
|
||||
*/
|
||||
private suspend fun addDocumentListener(tomlFile: VirtualFile, project: Project, module: VirtualFile) {
|
||||
readAction {
|
||||
tomlFile.findDocument()?.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
PyPackageCoroutine.launch(project) {
|
||||
val newVersion = poetryFindPythonVersionFromToml(tomlFile, project) ?: return@launch
|
||||
val oldVersion = PoetryPyProjectTomlPythonVersionsService.instance.getVersionString(module)
|
||||
if (oldVersion != newVersion) {
|
||||
PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, newVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, PoetryPyProjectTomlPythonVersionsService.instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the Python version string specified in the toml file.
|
||||
*
|
||||
@@ -0,0 +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.poetry
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.util.FileName
|
||||
import com.jetbrains.python.sdk.findAmongRoots
|
||||
import java.nio.file.Path
|
||||
|
||||
val POETRY_LOCK: FileName = FileName("poetry.lock")
|
||||
|
||||
suspend fun findPoetryLock(module: Module): Path? = module.findAmongRoots(POETRY_LOCK)
|
||||
@@ -0,0 +1,59 @@
|
||||
//// 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.poetry
|
||||
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.findDocument
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
|
||||
import com.jetbrains.python.sdk.PythonSdkUpdater
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* This class represents a post-startup activity for PyProjectToml files in a project.
|
||||
* It finds valid python versions in PyProjectToml files and saves them in PyProjectTomlPythonVersionsService.
|
||||
*/
|
||||
private class PoetryPyProjectTomlPostStartupActivity : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
val modulesRoots = PythonSdkUpdater.getModuleRoots(project)
|
||||
for (module in modulesRoots) {
|
||||
val tomlFile = withContext(Dispatchers.IO) {
|
||||
module.findChild(PY_PROJECT_TOML)?.let { getPyProjectTomlForPoetry(it) }
|
||||
} ?: continue
|
||||
val versionString = poetryFindPythonVersionFromToml(tomlFile, project) ?: continue
|
||||
|
||||
PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, versionString)
|
||||
addDocumentListener(tomlFile, project, module)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a document listener to a given toml file.
|
||||
* Updates PyProjectTomlPythonVersionsService map if needed.
|
||||
*
|
||||
* @param tomlFile The VirtualFile representing the toml file.
|
||||
* @param project The Project in which the toml file exists.
|
||||
* @param module The VirtualFile representing the module.
|
||||
*/
|
||||
private suspend fun addDocumentListener(tomlFile: VirtualFile, project: Project, module: VirtualFile) {
|
||||
readAction {
|
||||
tomlFile.findDocument()?.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
PyPackageCoroutine.launch(project) {
|
||||
val newVersion = poetryFindPythonVersionFromToml(tomlFile, project) ?: return@launch
|
||||
val oldVersion = PoetryPyProjectTomlPythonVersionsService.instance.getVersionString(module)
|
||||
if (oldVersion != newVersion) {
|
||||
PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, newVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, PoetryPyProjectTomlPythonVersionsService.instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.poetry
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.util.FileName
|
||||
import com.jetbrains.python.sdk.findAmongRoots
|
||||
import java.nio.file.Path
|
||||
|
||||
val POETRY_TOML: FileName = FileName("poetry.toml")
|
||||
|
||||
internal suspend fun findPoetryToml(module: Module): Path? = module.findAmongRoots(POETRY_TOML)
|
||||
|
||||
@@ -29,9 +29,9 @@ import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Error
|
||||
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Invisible
|
||||
import com.jetbrains.python.sdk.add.v2.getBasePath
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPythonVersionsService
|
||||
import com.jetbrains.python.poetry.PoetryPyProjectTomlPythonVersionsService
|
||||
import com.jetbrains.python.sdk.poetry.configurePoetryEnvironment
|
||||
import com.jetbrains.python.sdk.poetry.poetryToml
|
||||
import com.jetbrains.python.poetry.findPoetryToml
|
||||
import com.jetbrains.python.sdk.poetry.setupPoetrySdk
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -151,7 +151,7 @@ private class PoetryConfigService : SerializablePersistentStateComponent<PoetryC
|
||||
}
|
||||
|
||||
suspend fun setInProjectEnv(module: Module) {
|
||||
val hasPoetryToml = poetryToml(module) != null
|
||||
val hasPoetryToml = findPoetryToml(module) != null
|
||||
if (state.isInProjectEnv || hasPoetryToml) {
|
||||
val modulePath = withContext(Dispatchers.IO) {
|
||||
PyProjectToml.findFile(module)?.parent?.toNioPath() ?: module.basePath?.let { Path.of(it) }
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PySdkBundle
|
||||
import com.jetbrains.python.PythonPluginDisposable
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
|
||||
import com.jetbrains.python.projectModel.uv.UvProjectModelService
|
||||
import com.jetbrains.python.sdk.PySdkPopupFactory
|
||||
@@ -27,6 +28,7 @@ import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsI
|
||||
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
|
||||
import com.jetbrains.python.sdk.configurePythonSdk
|
||||
import com.jetbrains.python.sdk.uv.isUv
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -49,10 +51,14 @@ object PyProjectSdkConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> Sdk?): Boolean {
|
||||
suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> PyResult<Sdk?>): Boolean {
|
||||
thisLogger().debug("Configuring sdk with ${extension.javaClass.canonicalName} extension")
|
||||
|
||||
val sdk = supplier() ?: return false
|
||||
val sdk = supplier().getOr {
|
||||
ShowingMessageErrorSync.emit(it.error)
|
||||
return true
|
||||
} ?: return false
|
||||
|
||||
// TODO Move this to PyUvSdkConfiguration, show better notification
|
||||
if (sdk.isUv && Registry.`is`("python.project.model.uv", false)) {
|
||||
val ws = UvProjectModelService.findWorkspace(module)
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.SdkAdditionalData
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.poetry.PoetryPyProjectTomlPythonVersionsService
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.poetry.quickFixes.PoetryAssociationQuickFix
|
||||
import org.jdom.Element
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.jetbrains.python.PythonModuleTypeBase
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.poetry.getPyProjectTomlForPoetry
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.createSdk
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
|
||||
import com.jetbrains.python.poetry.getPyProjectTomlForPoetry
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.add.PyAddNewEnvPanel
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
|
||||
Reference in New Issue
Block a user