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 <Egor.Eliseev@jetbrains.com>

(cherry picked from commit ed85efe339518cdf6949752be286172113931c22)

IJ-MR-149143

GitOrigin-RevId: 3a682aac846e4a80e8310a3dcc0a5f1c988d28e2
This commit is contained in:
Egor Eliseev
2024-11-15 14:04:44 +00:00
committed by intellij-monorepo-bot
parent b6fd54065e
commit cf06be9b95
5 changed files with 77 additions and 3 deletions

View File

@@ -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

View File

@@ -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<String> = 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<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk> =
setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages)
override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk> {
module?.let { service<PoetryConfigService>().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<PoetryConfigService>().state::isInProjectEnv)
}
}
}
}
@Service(Service.Level.APP)
@State(name = "PyPoetrySettings", storages = [Storage("pyPoetrySettings.xml")])
internal class PoetryConfigService(private val cs: CoroutineScope) : PersistentStateComponent<PoetryConfigService> {
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")
}
}
}

View File

@@ -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 {

View File

@@ -190,6 +190,19 @@ suspend fun poetryReloadPackages(sdk: Sdk): Result<String> {
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<String> {
val executionResult = runPoetry(projectPath, "env", "list", "--full-path")
return executionResult.getOrNull()?.lineSequence()?.map { it.split(" ")[0] }?.filterNot { it.isEmpty() }?.toList() ?: emptyList()

View File

@@ -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.