From cf06be9b95371901a06f87ee53fb27576783c991 Mon Sep 17 00:00:00 2001 From: Egor Eliseev Date: Fri, 15 Nov 2024 14:04:44 +0000 Subject: [PATCH] PY-52265 Poetry in-project environment Add the new setting: create poetry env in the project. Merge-request: IJ-MR-149143 Merged-by: Egor Eliseev (cherry picked from commit ed85efe339518cdf6949752be286172113931c22) IJ-MR-149143 GitOrigin-RevId: 3a682aac846e4a80e8310a3dcc0a5f1c988d28e2 --- .../messages/PyBundle.properties | 1 + .../sdk/add/v2/PoetryNewEnvironmentCreator.kt | 59 ++++++++++++++++++- .../add/v2/PythonAddLocalInterpreterDialog.kt | 2 +- .../sdk/poetry/PoetryCommandExecutor.kt | 13 ++++ .../python/sdk/poetry/PoetryFilesUtils.kt | 5 ++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 785ff5ebcc82..4acd8f38b0e6 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -393,6 +393,7 @@ python.sdk.dialog.message.creating.virtual.environments.based.on.poetry.environm python.sdk.poetry.environment.panel.title=Poetry Environment python.sdk.poetry.install.packages.from.toml.checkbox.text=Install packages from pyproject.toml python.sdk.poetry.dialog.message.poetry.interpreter.has.been.already.added=Poetry interpreter has been already added, select ''{0}'' +python.sdk.poetry.dialog.add.new.environment.in.project.checkbox=Create an in-project environment python.sdk.pipenv.has.been.selected=Pipenv interpreter has been already added, select ''{0}'' in your interpreters list python.sdk.there.is.no.interpreter=No interpreter diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt index 7560ef67fb9e..f4e27405ba87 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt @@ -2,25 +2,48 @@ package com.jetbrains.python.sdk.add.v2 import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.validation.DialogValidationRequestor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindSelected import com.intellij.util.text.nullize +import com.intellij.util.xmlb.XmlSerializerUtil +import com.jetbrains.python.PyBundle import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.baseDir import com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPythonVersionsService import com.jetbrains.python.PythonHelpersLocator +import com.jetbrains.python.sdk.basePath +import com.jetbrains.python.sdk.poetry.configurePoetryEnvironment import com.jetbrains.python.sdk.poetry.poetryPath +import com.jetbrains.python.sdk.poetry.poetryToml +import com.jetbrains.python.sdk.poetry.pyProjectToml import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress import com.jetbrains.python.statistics.InterpreterType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext +import java.nio.file.Path class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("poetry", model) { override val interpreterType: InterpreterType = InterpreterType.POETRY override val executable: ObservableMutableProperty = model.state.poetryExecutable override val installationScript = PythonHelpersLocator.findPathInHelpers("pycharm_poetry_installer.py") + override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) { + super.buildOptions(panel, validationRequestor) + addInProjectCheckbox(panel) + } + override fun onShown() { val moduleDir = when (moduleOrProject) { is ModuleOrProject.ModuleAndProject -> moduleOrProject.module.baseDir @@ -44,10 +67,42 @@ class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, PropertiesComponent.getInstance().poetryPath = executable.get().nullize() } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = - setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages) + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + module?.let { service().setInProjectEnv(it) } + return setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages) + } override suspend fun detectExecutable() { model.detectPoetryExecutable() } + + private fun addInProjectCheckbox(panel: Panel) { + with(panel) { + row { + checkBox(PyBundle.message("python.sdk.poetry.dialog.add.new.environment.in.project.checkbox")) + .bindSelected(service().state::isInProjectEnv) + } + } + } +} + +@Service(Service.Level.APP) +@State(name = "PyPoetrySettings", storages = [Storage("pyPoetrySettings.xml")]) +internal class PoetryConfigService(private val cs: CoroutineScope) : PersistentStateComponent { + var isInProjectEnv: Boolean = false + + override fun getState(): PoetryConfigService = this + + override fun loadState(state: PoetryConfigService) { + XmlSerializerUtil.copyBean(state, this) + } + + suspend fun setInProjectEnv(module: Module) { + val hasPoetryToml = poetryToml(module) != null + + if (isInProjectEnv || hasPoetryToml) { + val modulePath = withContext(Dispatchers.IO) { pyProjectToml(module)?.parent }?.toNioPath() ?: module.basePath?.let { Path.of(it) } + configurePoetryEnvironment(modulePath, "virtualenvs.in-project", isInProjectEnv.toString(), "--local") + } + } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt index e2f4970c86cc..50bdf2d353ff 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt @@ -53,7 +53,7 @@ class PythonAddLocalInterpreterDialog(private val dialogPresenter: PythonAddLoca // The whole idea of context passing is doubtful Dispatchers.EDT + ModalityState.any().asContextElement(), ProjectPathFlows.create(basePath))) model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM) - mainPanel = PythonAddCustomInterpreter(model, errorSink = errorSink) + mainPanel = PythonAddCustomInterpreter(model, moduleOrProject = dialogPresenter.moduleOrProject, errorSink = errorSink) mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath))) }.apply { diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt index 612c4b340f4e..caa5edfc7ed2 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt @@ -190,6 +190,19 @@ suspend fun poetryReloadPackages(sdk: Sdk): Result { return runPoetryWithSdk(sdk, "show") } +/** + * Configures the Poetry environment for the specified module path with the given arguments. + * Runs command: GeneralCommandLine("poetry config [args]").withWorkingDirectory([modulePath]) + * + * @param [modulePath] The path to the module where the Poetry environment is to be configured. + * Can be null, in which case the global Poetry environment will be configured. + * @param [args] A vararg array of String arguments to pass to the Poetry configuration command. + */ +@Internal +suspend fun configurePoetryEnvironment(modulePath: Path?, vararg args: String) { + runPoetry(modulePath, "config", *args) +} + private suspend fun getPoetryEnvs(projectPath: Path): List { val executionResult = runPoetry(projectPath, "env", "list", "--full-path") return executionResult.getOrNull()?.lineSequence()?.map { it.split(" ")[0] }?.filterNot { it.isEmpty() }?.toList() ?: emptyList() diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt index 649d07493306..900899e9d9ce 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt @@ -27,6 +27,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.vfs.findDocument import com.intellij.openapi.vfs.findPsiFile @@ -55,6 +56,7 @@ import java.util.concurrent.ConcurrentMap const val PY_PROJECT_TOML: String = "pyproject.toml" const val POETRY_LOCK: String = "poetry.lock" +const val POETRY_TOML: String = "poetry.toml" const val POETRY_DEFAULT_SOURCE_URL: String = "https://pypi.org/simple" val LOGGER = Logger.getInstance("#com.jetbrains.python.sdk.poetry") @@ -81,6 +83,9 @@ private suspend fun poetryLock(module: Module) = withContext(Dispatchers.IO) { f @Internal suspend fun pyProjectToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) { findAmongRoots(module, PY_PROJECT_TOML) } +internal suspend fun poetryToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) { + findAmongRoots(module, POETRY_TOML)?.takeIf { readAction { ProjectFileIndex.getInstance(module.project).isInProject(it) } } +} /** * Watches for edits in PyProjectToml inside modules with a poetry SDK set.