PY-52688 migrate Python tool SDK metadata to shared fields

Back uv, Poetry, Pipenv, and venv SDKs with the common working-directory
model while preserving legacy serialized fields for rollback
compatibility. Target wrappers copy only project execution metadata, and
uv derives its environment root from the SDK interpreter path instead of
persisted flavor data.


(cherry picked from commit 2f7f3d1b4eeb079736ef838e72a0c34feba4c01b)

IJ-MR-215198

GitOrigin-RevId: 4c93917f2003728299513c7fec6a5a3e7e6d59e1
This commit is contained in:
Alexey Katsman
2026-07-24 21:01:10 +00:00
committed by intellij-monorepo-bot
parent 807f9c632b
commit d9505303c4
29 changed files with 222 additions and 137 deletions
@@ -14,8 +14,11 @@ import java.nio.file.Path
@ApiStatus.Internal
class PyPoetrySdkAdditionalData : PythonSdkAdditionalData {
constructor(associatedModulePath: Path?) : super(PyFlavorAndData(PyFlavorData.Empty, PyPoetrySdkFlavor)) {
this.associatedModulePath = associatedModulePath?.toString()
constructor(workingDirectory: Path) : super(
PyFlavorAndData(PyFlavorData.Empty, PyPoetrySdkFlavor),
workingDirectory,
) {
associatedModulePath = workingDirectory.toString()
}
override fun save(element: Element) {
@@ -34,7 +37,7 @@ class PyPoetrySdkAdditionalData : PythonSdkAdditionalData {
fun load(element: Element): PyPoetrySdkAdditionalData? =
when {
element.getAttributeValue(IS_POETRY) == "true" -> {
PyPoetrySdkAdditionalData(null).apply {
PyPoetrySdkAdditionalData(Path.of("")).apply {
load(element)
}
}
@@ -1,7 +1,9 @@
// 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.poetry
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.flavors.PythonFlavorProvider
@@ -20,6 +22,14 @@ object PyPoetrySdkFlavor : CPythonSdkFlavor<PyFlavorData.Empty>() {
override fun getIcon(): Icon = PythonCommunityImplPoetryCommonIcons.Poetry
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
override fun migrateAdditionalData(
additionalData: PythonSdkAdditionalData,
data: PyFlavorData.Empty,
): AdditionalDataMigration<PyFlavorData.Empty> {
val workingDirectory = additionalData.associatedModulePath?.takeIf { it.isNotBlank() }?.toNioPathOrNull()
return AdditionalDataMigration(data, workingDirectory)
}
override fun isValidSdkPath(pythonBinaryPath: Path): Boolean = false
}
@@ -41,6 +41,7 @@ python.sdk.running.progress.text=Running {0}\u2026
python.sdk.running.one.minute.progress.details=About 1 minute left
python.sdk.running.sudo.prompt=Enter your password to install {0}
python.sdk.cannot.create.tool.should.be.installed=Cannot create SDK without a proper tool installed
python.sdk.cannot.create.working.directory.empty=Cannot create SDK without working directory
# PLEASE add only the keys that are supposed to be re-used by plugins,
# PLEASE keep keys below grouped by the topic,
@@ -26,6 +26,7 @@ interface FileSystem<P : PathHolder> {
val isReadOnly: Boolean
val isBrowsable: Boolean
val isLocal: Boolean
val toolPathCanBePersisted: Boolean
val userReadableName: @NonNls String
val platformAndRoot: PlatformAndRoot
@@ -69,7 +70,7 @@ interface FileSystem<P : PathHolder> {
suspend fun detectSelectableVenv(projectPathPrefix: Path): List<DetectedSelectableInterpreter<P>>
fun preferredInterpreterBasePath(): P? = null
fun resolvePythonBinary(pythonHome: P): P?
fun resolvePythonHome(pythonBinary: P): P
fun resolvePythonHome(pythonHomeOrBinary: P): P
fun getVenvName(pythonHome: P): String?
fun getBinaryToExec(path: P, workingDir: Path? = null): BinaryToExec
@@ -26,9 +26,12 @@ internal const val PYCHARM_HELPERS: String = ".pycharm_helpers"
open class PyTargetAwareAdditionalData private constructor(
private val b: RemoteSdkPropertiesHolder,
flavorAndData: PyFlavorAndData<*, *>?,
flavorAndData: PyFlavorAndData<*, *>,
workingDirectory: Path,
requirementsFile: String?,
targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null,
) : PythonSdkAdditionalData(flavorAndData), TargetBasedSdkAdditionalData, RemoteSdkProperties by b, PyRemoteSdkAdditionalDataMarker {
) : PythonSdkAdditionalData(flavorAndData, workingDirectory), TargetBasedSdkAdditionalData, RemoteSdkProperties by b,
PyRemoteSdkAdditionalDataMarker {
/**
* The source of truth for the target configuration.
*/
@@ -47,14 +50,28 @@ open class PyTargetAwareAdditionalData private constructor(
}
init {
this.requirementsFile = requirementsFile
notifyTargetEnvironmentConfigurationChanged()
}
constructor(flavorAndData: PyFlavorAndData<*, *>, targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null) : this(
RemoteSdkPropertiesHolder(
PYCHARM_HELPERS),
@ApiStatus.Internal
constructor(
flavorAndData: PyFlavorAndData<*, *>,
workingDirectory: Path,
targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null,
requirementsFile: String? = null,
) : this(
RemoteSdkPropertiesHolder(PYCHARM_HELPERS),
flavorAndData,
targetEnvironmentConfiguration)
workingDirectory,
requirementsFile,
targetEnvironmentConfiguration,
)
@ApiStatus.Internal
constructor(additionalData: PythonSdkAdditionalData, targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null) : this(
additionalData.flavorAndData, additionalData.workingDirectory, targetEnvironmentConfiguration, additionalData.requirementsFile
)
override fun save(rootElement: Element) { // store "interpreter paths" (i.e. `PYTHONPATH` elements)
super.save(rootElement) // store `INTERPRETER_PATH`, `HELPERS_PATH`, etc
@@ -136,7 +153,8 @@ open class PyTargetAwareAdditionalData private constructor(
}
// TODO Python flavor identifier must be stored in `element` and taken from it here
val data = PyTargetAwareAdditionalData(flavorAndData = PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()))
val data = PyTargetAwareAdditionalData(flavorAndData = PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()),
workingDirectory = Path.of(""))
data.interpreterPath = homePath
data.load(element)
// TODO [targets] Load `SKELETONS_PATH` for Target-based Python SDK from `Element`
@@ -149,7 +167,8 @@ open class PyTargetAwareAdditionalData private constructor(
}
private class DummyTargetAwareAdditionalData(base: Element) :
PyTargetAwareAdditionalData(flavorAndData = PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance())),
PyTargetAwareAdditionalData(flavorAndData = PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()),
workingDirectory = Path.of("")),
PyRemoteSdkAdditionalDataMarker {
val element: Element = base.clone()
@@ -33,8 +33,7 @@ suspend fun PyEnvironmentFactory.createSdk(request: SdkCreationRequest): Pair<Sd
Pair(sdk, environment)
}
is SdkCreationRequest.RemotePython -> {
val targetData = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()),
request.targetConfig).apply {
val targetData = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()), workingDir, request.targetConfig).apply {
interpreterPath = PYTHON_PATH_ON_TARGET
}
try {
@@ -6,7 +6,7 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class CachingPyEnvironmentFactory(
private val wrapper: PyEnvironmentFactory,
) : PyEnvironmentFactory {
) : PyEnvironmentFactory by wrapper {
private val cache = PyEnvironmentCache()
@@ -11,7 +11,7 @@ import java.nio.file.Path
* matching one.
*/
class DefaultPyEnvironmentFactory(
private val workingDir: Path,
override val workingDir: Path,
private val providers: List<Pair<(PyEnvironmentSpec<*>) -> Boolean, PyEnvironmentProvider<*>>>
): PyEnvironmentFactory {
@@ -2,10 +2,13 @@
package com.intellij.python.test.env.core
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@ApiStatus.Internal
interface PyEnvironmentFactory: AutoCloseable {
val workingDir: Path
suspend fun createEnvironment(spec: PyEnvironmentSpec<*>): PyEnvironment {
return createEnvironment(this, spec)
}
@@ -26,26 +26,32 @@ import java.nio.file.Path
* Create virtual env in [where]. If [addToSdkTable] then also added to the project jdk table
*/
fun TestFixture<SdkFixture<PyEnvironment>>.pyVenvFixture(
where: TestFixture<Path>,
addToSdkTable: Boolean,
moduleFixture: TestFixture<Module>? = null,
where: TestFixture<Path>,
addToSdkTable: Boolean,
moduleFixture: TestFixture<Module>? = null,
): TestFixture<Sdk> = testFixture {
val env = this@pyVenvFixture.init().env
withContext(Dispatchers.EDT) {
val module = moduleFixture?.init()
val venvDir = where.init().resolve(".venv")
val venvPython = createVenv(env.pythonPath, venvDir).getOrThrow()
val venvSdk = createSdk(PathHolder.Eel(venvPython), createVenvAdditionalData(), advancedOpts = SdkCreationAdvancedOpts(persist = addToSdkTable)).orThrow()
if (addToSdkTable) {
if (module != null) {
module.pythonSdk = venvSdk
venvSdk.setAssociationToModule(module)
}
}
initialized(venvSdk) {
edtWriteAction {
ProjectJdkTable.getInstance().removeJdk(venvSdk)
}
}
val env = this@pyVenvFixture.init().env
withContext(Dispatchers.EDT) {
val module = moduleFixture?.init()
val workingDirectory = where.init()
val venvDir = workingDirectory.resolve(".venv")
val venvPython = createVenv(env.pythonPath, venvDir).getOrThrow()
val additionalData = createVenvAdditionalData(workingDirectory)
val venvSdk = createSdk(
PathHolder.Eel(venvPython),
additionalData,
advancedOpts = SdkCreationAdvancedOpts(persist = addToSdkTable),
).orThrow()
if (addToSdkTable) {
if (module != null) {
module.pythonSdk = venvSdk
venvSdk.setAssociationToModule(module)
}
}
initialized(venvSdk) {
edtWriteAction {
ProjectJdkTable.getInstance().removeJdk(venvSdk)
}
}
}
}
@@ -11,31 +11,26 @@ import kotlin.io.path.pathString
@ApiStatus.Internal
class UvSdkAdditionalData : PythonSdkAdditionalData {
val flavorData: UvSdkFlavorData
get() = flavorAndData.data as UvSdkFlavorData
constructor(uvWorkingDirectory: Path?, usePip: Boolean?, venvPath: FullPathOnTarget?, uvPath: FullPathOnTarget?) : this(UvSdkFlavorData(
uvWorkingDirectory,
usePip,
venvPath,
uvPath))
constructor(
uvWorkingDirectory: Path,
usePip: Boolean?,
venvPath: FullPathOnTarget?,
uvPath: FullPathOnTarget?,
) : this(UvSdkFlavorData(uvWorkingDirectory, usePip, venvPath, uvPath), uvWorkingDirectory)
private constructor(flavorData: UvSdkFlavorData) : super(PyFlavorAndData(flavorData, UvSdkFlavor)) {
this.flavorData = flavorData
}
constructor(data: PythonSdkAdditionalData) : super(data) {
when (data) {
is UvSdkAdditionalData -> this.flavorData = data.flavorData
else -> this.flavorData = UvSdkFlavorData(null, null, null, null)
}
}
private constructor(
legacyFlavorData: UvSdkFlavorData, uvWorkingDirectory: Path,
) : super(PyFlavorAndData(legacyFlavorData, UvSdkFlavor), uvWorkingDirectory)
override fun save(element: Element) {
super.save(element)
element.setAttribute(IS_UV, "true")
// keep backward compatibility with old data
if (flavorData.uvWorkingDirectory?.pathString?.isNotBlank() == true) {
element.setAttribute(UV_WORKING_DIR, flavorData.uvWorkingDirectory.pathString)
val persistedWorkingDirectory = workingDirectory.takeIf { hasValidWorkingDirectory() } ?: flavorData.uvWorkingDirectory
if (persistedWorkingDirectory != null) {
element.setAttribute(UV_WORKING_DIR, persistedWorkingDirectory.pathString)
}
if (flavorData.usePip == true) {
@@ -62,22 +57,22 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
fun load(element: Element): UvSdkAdditionalData? {
return when {
element.getAttributeValue(IS_UV) == "true" -> {
val uvWorkingDirectory =
if (element.getAttributeValue(UV_WORKING_DIR).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_WORKING_DIR))
val uvWorkingDirectory = if (element.getAttributeValue(UV_WORKING_DIR).isNullOrEmpty()) {
Path.of("")
}
else {
Path.of(element.getAttributeValue(UV_WORKING_DIR))
}
val usePip = element.getAttributeValue(USE_PIP)?.toBoolean()
val venvPath = if (element.getAttributeValue(UV_VENV_PATH).isNullOrEmpty()) null else element.getAttributeValue(UV_VENV_PATH)
val venvPath = element.getAttributeValue(UV_VENV_PATH)?.takeIf { it.isNotBlank() }
val uvPath = if (element.getAttributeValue(UV_TOOL_PATH).isNullOrEmpty()) null else element.getAttributeValue(UV_TOOL_PATH)
UvSdkAdditionalData(uvWorkingDirectory, usePip, venvPath, uvPath).apply {
val legacyFlavorData = UvSdkFlavorData(uvWorkingDirectory, usePip, venvPath, uvPath)
UvSdkAdditionalData(legacyFlavorData, uvWorkingDirectory).apply {
load(element)
}
}
else -> null
}
}
@JvmStatic
fun copy(data: PythonSdkAdditionalData): UvSdkAdditionalData {
return UvSdkAdditionalData(data)
}
}
}
@@ -1,14 +1,37 @@
package com.jetbrains.python.sdk.uv
import com.intellij.python.uv.common.icons.PythonUvCommonIcons
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
import javax.swing.Icon
internal object UvSdkFlavor : CPythonSdkFlavor<UvSdkFlavorData>() {
@ApiStatus.Internal
object UvSdkFlavor : CPythonSdkFlavor<UvSdkFlavorData>() {
override fun getIcon(): Icon = PythonUvCommonIcons.UV
override fun getFlavorDataClass(): Class<UvSdkFlavorData> = UvSdkFlavorData::class.java
override fun migrateAdditionalData(
additionalData: PythonSdkAdditionalData,
data: UvSdkFlavorData,
): AdditionalDataMigration<UvSdkFlavorData> {
val workingDirectory = additionalData.workingDirectory.takeIf { additionalData.hasValidWorkingDirectory() }
?: data.uvWorkingDirectory
val migratedData = data.copy(
uvWorkingDirectory = workingDirectory,
usePip = data.usePip,
venvPath = data.venvPath,
uvPath = data.uvPath,
)
return AdditionalDataMigration(migratedData, workingDirectory)
}
override fun withWorkingDirectory(
data: UvSdkFlavorData,
workingDirectory: Path,
): UvSdkFlavorData = data.copy(uvWorkingDirectory = workingDirectory)
override fun isValidSdkPath(pythonBinaryPath: Path): Boolean {
return false
}
@@ -10,7 +10,13 @@ import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
// TODO PY-87712 Move to a separate storage
/**
* TODO PY-87712 Should drop as a whole
* uvWorkingDirectory - workingDirectory in PythonSdkAdditionalData
* usePip - can be deduced based on requirementsFile in PythonSdkAdditionalData
* venvPath - sdkHome
* uvPath - stored as a setting for local EELs, for targets we can use detection only
*/
@ApiStatus.Internal
data class UvSdkFlavorData(
val uvWorkingDirectory: Path?,
@@ -25,9 +31,12 @@ data class UvSdkFlavorData(
throw IllegalArgumentException("Sdk ${sdk} doesn't have interpreter path set")
}
targetCommandLineBuilder.setExePath(interpreterPath)
targetCommandLineBuilder.addEnvironmentVariable("UV_PROJECT_ENVIRONMENT", venvPath)
val separator = targetCommandLineBuilder.request.targetPlatform.platform.fileSeparator
targetCommandLineBuilder.addEnvironmentVariable("UV_PROJECT_ENVIRONMENT", interpreterPath.parentPath(separator).parentPath(separator))
if (!PythonSdkUtil.isRemote(sdk)) {
PySdkUtil.activateVirtualEnv(sdk)
}
}
private fun String.parentPath(separator: Char): String = removeSuffix(separator.toString()).substringBeforeLast(separator)
}
@@ -3,6 +3,7 @@ package com.intellij.python.venv
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
import com.intellij.platform.eel.provider.asEelPath
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.community.execService.ExecOptions
@@ -17,17 +18,21 @@ import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.getOr
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.sdk.workingDirectory
import com.jetbrains.python.venvReader.Directory
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.CheckReturnValue
import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.minutes
@@ -99,8 +104,18 @@ const val VIRTUALENV_ZIPAPP_NAME: HelperName = "virtualenv-py3.pyz"
val MINIMUM_SUPPORTED_VENV_PYTHON_VERSION: LanguageLevel = LanguageLevel.PYTHON38
/**
* Creates [PythonSdkAdditionalData] for virtual env
* Creates [PythonSdkAdditionalData] for virtual env using working directory
*/
@Internal
fun createVenvAdditionalData(): PythonSdkAdditionalData =
PythonSdkAdditionalData(PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance()))
fun createVenvAdditionalData(workingDirectory: Path): PythonSdkAdditionalData =
PythonSdkAdditionalData(PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance()), workingDirectory)
/**
* Creates [PythonSdkAdditionalData] for virtual env using module baseDir as working directory
*/
@Internal
fun createVenvAdditionalData(module: Module): PyResult<PythonSdkAdditionalData> {
return ModuleOrProject.ModuleAndProject(module).workingDirectory?.let {
PyResult.success(createVenvAdditionalData(it))
} ?: PyResult.localizedError(PySdkBundle.message("python.sdk.cannot.create.working.directory.empty"))
}
@@ -109,7 +109,7 @@ internal class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
val sdk = createSdk(
PathHolder.Eel(file.toNioPath()),
PyPipEnvSdkAdditionalData(),
PyPipEnvSdkAdditionalData(Path.of(basePath)),
suggestedSdkName(basePath)
).getOr { return@withBackgroundProgress it }
@@ -152,7 +152,7 @@ internal class PyPoetrySdkConfiguration : PyProjectTomlConfigurationExtension {
LOGGER.debug("Setting up associated poetry environment: $path, $basePath")
val sdk = createSdk(
PathHolder.Eel(file.toNioPath()),
PyPoetrySdkAdditionalData(module.baseDir?.path?.let { Path.of(it) }),
PyPoetrySdkAdditionalData(basePath),
suggestedSdkName(basePath)
).getOr { return@withBackgroundProgress it }
@@ -101,7 +101,6 @@ suspend fun createVenvAndSdk(
}
logger.info("using venv python $venvPython")
val sdk = getSdk(venvPython).getOr { return it }
if (moduleOrProject.moduleIfExists == null && project.modules.isEmpty()) {
edtWriteAction {
val projectPath = vfsPath.toNioPath()
@@ -111,6 +110,7 @@ suspend fun createVenvAndSdk(
}
val module = moduleOrProject.moduleIfExists ?: project.modules.first()
ensureModuleHasRoot(module, vfsPath)
val sdk = getSdk(venvPython, module).getOr { return it }
withContext(Dispatchers.IO) {
// generated files should be readable by VFS
VfsUtil.markDirtyAndRefresh(false, true, true, vfsPath)
@@ -192,11 +192,12 @@ private suspend fun ensureModuleHasRoot(module: Module, root: VirtualFile): Unit
}
}
private suspend fun getSdk(pythonPath: PythonBinary): PyResult<Sdk> =
private suspend fun getSdk(pythonPath: PythonBinary, module: Module): PyResult<Sdk> =
withProgressText(ProjectBundle.message("progress.text.configuring.sdk")) {
val allJdks = PythonSdkUtil.getAllSdks().toTypedArray()
val currentSdk = allJdks.firstOrNull { sdk -> sdk.homeDirectory?.toNioPath() == pythonPath }
if (currentSdk != null) return@withProgressText PyResult.success(currentSdk)
return@withProgressText createSdk(PathHolder.Eel(pythonPath), createVenvAdditionalData())
val additionalData = createVenvAdditionalData(module).getOr { return@withProgressText it }
return@withProgressText createSdk(PathHolder.Eel(pythonPath), additionalData)
}
@@ -109,6 +109,7 @@ data class EelFileSystem(
override val isBrowsable: Boolean = true
override val isReadOnly: Boolean = false
override val isLocal: Boolean = eelApi == localEel
override val toolPathCanBePersisted: Boolean = isLocal
override val userReadableName: @NonNls String = eelApi.descriptor.name
override val platformAndRoot: PlatformAndRoot = eelApi.getPlatformAndRoot()
@@ -143,6 +144,7 @@ data class EelFileSystem(
targetPanelExtension: TargetPanelExtension?,
suggestedSdkName: String?,
): PyResult<Sdk> {
require(sdkAdditionalData.hasValidWorkingDirectory()) { "Python SDK working directory must be initialized before setup" }
return createSdk(pythonBinaryPath, sdkAdditionalData, suggestedSdkName)
}
@@ -279,8 +281,8 @@ data class EelFileSystem(
return pythonHome.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
}
override fun resolvePythonHome(pythonBinary: PathHolder.Eel): PathHolder.Eel {
return PathHolder.Eel(pythonBinary.path.resolvePythonHome())
override fun resolvePythonHome(pythonHomeOrBinary: PathHolder.Eel): PathHolder.Eel {
return PathHolder.Eel(pythonHomeOrBinary.path.resolvePythonHome())
}
override fun getVenvName(pythonHome: PathHolder.Eel): String? {
@@ -382,6 +384,7 @@ data class TargetFileSystem(
override val isBrowsable: Boolean
get() = targetEnvironmentConfiguration.getTargetType() is BrowsableTargetEnvironmentType
override val isLocal: Boolean = false
override val toolPathCanBePersisted: Boolean = false
override val userReadableName: @NonNls String = targetEnvironmentConfiguration.displayName
override val platformAndRoot: PlatformAndRoot = targetEnvironmentConfiguration.getPlatformAndRoot()
@@ -435,11 +438,11 @@ data class TargetFileSystem(
targetPanelExtension: TargetPanelExtension?,
suggestedSdkName: String?,
): PyResult<Sdk> {
require(sdkAdditionalData.hasValidWorkingDirectory()) { "Python SDK working directory must be initialized before setup" }
val languageLevel = getBinaryToExec(pythonBinaryPath).validatePythonAndGetInfo().getOr { return it }.languageLevel
val (additionalData, customSdkSuggestedName) = run {
val flavorAndData = sdkAdditionalData.flavorAndData
val data = PyTargetAwareAdditionalData(flavorAndData).also {
val data = PyTargetAwareAdditionalData(sdkAdditionalData, targetEnvironmentConfiguration).also {
it.interpreterPath = pythonBinaryPath.toString()
it.targetEnvironmentConfiguration = targetEnvironmentConfiguration
}
@@ -566,8 +569,17 @@ data class TargetFileSystem(
return PathHolder.Target(VirtualEnvReader().findPythonInPythonRootForTarget(pythonHomeString, platform))
}
override fun resolvePythonHome(pythonBinary: PathHolder.Target): PathHolder.Target {
return PathHolder.Target(pythonBinary.pathString.substringBeforeLast("/bin/"))
override fun resolvePythonHome(pythonHomeOrBinary: PathHolder.Target): PathHolder.Target {
val separator = targetEnvironmentConfiguration.getPlatformAndRoot().platform.fileSeparator
val path = pythonHomeOrBinary.pathString.removeSuffix(separator.toString())
val fileName = path.substringAfterLast(separator).lowercase()
val binaryDirectory = path.substringBeforeLast(separator)
val binaryDirectoryName = binaryDirectory.substringAfterLast(separator)
if (!fileName.startsWith("python") ||
!(binaryDirectoryName.equals("bin", ignoreCase = true) || binaryDirectoryName.equals("scripts", ignoreCase = true))) {
return pythonHomeOrBinary
}
return PathHolder.Target(binaryDirectory.substringBeforeLast(separator))
}
override fun getVenvName(pythonHome: PathHolder.Target): String? {
@@ -8,6 +8,7 @@ import com.intellij.python.uv.common.UV_UI_INFO
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.workingDirectory
import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
import com.jetbrains.python.sdk.add.v2.PathHolder
@@ -15,7 +16,6 @@ import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.ToolValidator
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.savePathForEelOnly
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal
import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk
import com.jetbrains.python.statistics.InterpreterType
@@ -37,12 +37,8 @@ internal class UvExistingEnvironmentSelector<P : PathHolder>(model: PythonMutabl
val selectedInterpreterPath =
sdkHomePath ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePath))
val associatedModule = extractModule(moduleOrProject)
val basePathString = associatedModule?.baseDir?.path
?: moduleOrProject.project.basePath
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", null))
val workingDir = Path.of(basePathString)
val workingDir = moduleOrProject.workingDirectory
?: return PyResult.localizedError(PyBundle.message("python.sdk.project.working.directory.not.found"))
return setupExistingEnvAndSdk(
pythonBinary = selectedInterpreterPath,
@@ -61,10 +57,4 @@ internal class UvExistingEnvironmentSelector<P : PathHolder>(model: PythonMutabl
}
}
}
private fun extractModule(moduleOrProject: ModuleOrProject): Module? =
when (moduleOrProject) {
is ModuleOrProject.ModuleAndProject -> moduleOrProject.module
else -> null
}
}
@@ -94,7 +94,7 @@ internal suspend fun PyCondaCommand.createCondaSdkFromExistingEnvironment(
SdkCreationRequest.EelSdk(Path(interpreterPath), PythonSdkAdditionalData(flavorAndData, workingDirectory))
}
else {
val addData = PyTargetAwareAdditionalData(flavorAndData, targetConfig).also {
val addData = PyTargetAwareAdditionalData(flavorAndData, workingDirectory, targetConfig).also {
it.interpreterPath = interpreterPath
}
SdkCreationRequest.TargetSdk(interpreterPath, addData)
@@ -84,7 +84,7 @@ internal suspend fun setupPipEnvSdkWithProgressReport(
return createSdk(
pythonBinaryPath = PathHolder.Eel(pythonExecutablePath),
sdkAdditionalData = PyPipEnvSdkAdditionalData()
sdkAdditionalData = PyPipEnvSdkAdditionalData(moduleBasePath)
)
}
@@ -2,15 +2,17 @@
package com.jetbrains.python.sdk.pipenv
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
import org.jdom.Element
import java.nio.file.Path
/**
* Additional Pipenv data associated with an SDK.
*
*/
class PyPipEnvSdkAdditionalData : PythonSdkAdditionalData {
constructor() : super(PyPipEnvSdkFlavor)
constructor(data: PythonSdkAdditionalData) : super(data)
constructor(workingDirectory: Path) : super(PyFlavorAndData(PyFlavorData.Empty, PyPipEnvSdkFlavor), workingDirectory)
override fun save(element: Element) {
super.save(element)
@@ -28,18 +30,11 @@ class PyPipEnvSdkAdditionalData : PythonSdkAdditionalData {
fun load(element: Element): PyPipEnvSdkAdditionalData? =
when {
element.getAttributeValue(IS_PIPENV) == "true" -> {
PyPipEnvSdkAdditionalData().apply {
PyPipEnvSdkAdditionalData(Path.of("")).apply {
load(element)
}
}
else -> null
}
/**
* Creates a new instance of data with copied fields.
*/
@JvmStatic
fun copy(data: PythonSdkAdditionalData): PyPipEnvSdkAdditionalData =
PyPipEnvSdkAdditionalData(data)
}
}
@@ -8,7 +8,6 @@ import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.execService.python.validatePythonAndGetInfo
import com.intellij.python.community.impl.poetry.common.poetryPath
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.PythonHomePath
import com.jetbrains.python.errorProcessing.ErrorSink
@@ -29,9 +28,9 @@ import com.jetbrains.python.sdk.add.v2.EelFileSystem
import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.toEelFileSystem
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.sdk.pyRichSdkAsync
import com.jetbrains.python.sdk.pySdkAdditionalData
import com.jetbrains.python.sdk.runTool
import com.jetbrains.python.venvReader.VirtualEnvReader
import io.github.z4kn4fein.semver.Version
@@ -40,7 +39,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tuweni.toml.Toml
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.Nls
import java.nio.file.Path
import kotlin.io.path.name
import kotlin.io.path.pathString
@@ -48,7 +46,6 @@ import kotlin.io.path.pathString
/**
* This source code is edited by @koxudaxi Koudai Aono <koxudaxi@gmail.com>
*/
private val poetryNotFoundException: @Nls String = PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message")
private val VERSION_2 = "2.0.0".toVersion()
@@ -103,8 +100,7 @@ internal suspend fun getPoetryExecutable(eel: EelApi = localEel): Path? =
*/
@Internal
internal suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): PyResult<String> {
val projectPath = sdk.associatedModulePath?.let { Path.of(it) }
?: return PyResult.localizedError(poetryNotFoundException) // Choose a correct sdk
val projectPath = sdk.pySdkAdditionalData.workingDirectory
val pythonHomePath = sdk.pyRichSdkAsync().pythonHomePath
?: return PyResult.localizedError(PySdkBundle.message("python.sdk.broken.configuration", sdk.name))
val env = buildMap {
@@ -26,6 +26,7 @@ import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.TargetFileSystem
import com.jetbrains.python.sdk.pySdkAdditionalData
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pySdkAdditionalData
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
@@ -90,7 +91,7 @@ internal sealed interface UvExecutionContext<P : PathHolder> {
private suspend fun createEelUvExecutionContext(
workingDir: Path,
venvPathString: String?,
pythonBinaryPath: String,
uvPathString: String?,
): UvExecutionContext.Eel {
val eelApi = workingDir.getEelDescriptor().toEelApi()
@@ -98,7 +99,7 @@ private suspend fun createEelUvExecutionContext(
val uvPath = getUvExecutable(fileSystem, uvPathString)
return UvExecutionContext.Eel(
workingDir = workingDir,
venvPath = venvPathString?.let { PathHolder.Eel(Path.of(it)) },
venvPath = fileSystem.resolvePythonHome(PathHolder.Eel(Path.of(pythonBinaryPath))),
fileSystem = fileSystem,
uvPath = uvPath
)
@@ -106,7 +107,7 @@ private suspend fun createEelUvExecutionContext(
private suspend fun createTargetUvExecutionContext(
workingDir: Path,
venvPathString: FullPathOnTarget?,
pythonBinaryPath: FullPathOnTarget,
uvPathString: FullPathOnTarget?,
targetConfig: TargetEnvironmentConfiguration,
): UvExecutionContext.Target {
@@ -114,7 +115,7 @@ private suspend fun createTargetUvExecutionContext(
val uvPath = getUvExecutable(fileSystem, uvPathString)
return UvExecutionContext.Target(
workingDir = workingDir,
venvPath = venvPathString?.let { PathHolder.Target(it) },
venvPath = fileSystem.resolvePythonHome(PathHolder.Target(pythonBinaryPath)),
fileSystem = fileSystem,
uvPath = uvPath
)
@@ -122,23 +123,23 @@ private suspend fun createTargetUvExecutionContext(
internal fun Sdk.getUvExecutionContextAsync(scope: CoroutineScope, project: Project? = null): Deferred<UvExecutionContext<*>>? {
val data = sdkAdditionalData
val uvWorkingDirectory = uvFlavorData?.uvWorkingDirectory
val venvPathString = uvFlavorData?.venvPath
val uvWorkingDirectory = pySdkAdditionalData.workingDirectory.takeIf { pySdkAdditionalData.hasValidWorkingDirectory() }
val uvPathString = uvFlavorData?.uvPath
val pythonBinaryPath = homePath ?: return null
return when (data) {
is UvSdkAdditionalData -> {
val defaultWorkingDir = project?.basePath?.let { Path.of(it) }
val cwd = uvWorkingDirectory ?: defaultWorkingDir ?: return null
scope.async(start = CoroutineStart.LAZY) {
createEelUvExecutionContext(cwd, venvPathString, uvPathString)
createEelUvExecutionContext(cwd, pythonBinaryPath, uvPathString)
}
}
is PyTargetAwareAdditionalData -> {
val targetConfig = data.targetEnvironmentConfiguration ?: return null
val cwd = uvWorkingDirectory ?: return null
scope.async(start = CoroutineStart.LAZY) {
createTargetUvExecutionContext(cwd, venvPathString, uvPathString, targetConfig)
createTargetUvExecutionContext(cwd, pythonBinaryPath, uvPathString, targetConfig)
}
}
else -> null
@@ -223,7 +224,8 @@ internal suspend fun <P : PathHolder> setupExistingEnvAndSdk(
fileSystem: FileSystem<P>,
usePip: Boolean,
): PyResult<Sdk> = withProgressText(PyBundle.message("python.sdk.progress.uv.configuring")) {
val sdkAdditionalData = UvSdkAdditionalData(workingDir, usePip, fileSystem.resolvePythonHome(pythonBinary).toString(), uvPath.toString())
val venvPath = fileSystem.resolvePythonHome(pythonBinary).toString()
val sdkAdditionalData = UvSdkAdditionalData(workingDir, usePip, venvPath, uvPath.toString())
val sdk = fileSystem.setupSdk(null, pythonBinary, sdkAdditionalData, null, null)
sdk
}
@@ -15,10 +15,10 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
import com.jetbrains.python.onFailure
import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.sdk.pySdkAdditionalData
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.uv.uvFlavorData
import com.jetbrains.python.venvReader.tryResolvePath
import org.jdom.Element
import org.jetbrains.annotations.ApiStatus
@@ -39,14 +39,14 @@ data class UvRunConfigurationOptions(
var checkSync: Boolean = true,
var uvSdkKey: String? = null,
var uvArgs: List<String> = listOf(),
var debugJustMyCode: Boolean = false
var debugJustMyCode: Boolean = false,
) {
val uvSdk: Sdk?
get() = uvSdkKey?.let { PythonSdkUtil.findSdkByKey(it)}
get() = uvSdkKey?.let { PythonSdkUtil.findSdkByKey(it) }
val workingDirectory: Path?
get() = uvSdk?.uvFlavorData?.uvWorkingDirectory
?: tryResolvePath(uvSdk?.associatedModulePath)
get() = uvSdk?.pySdkAdditionalData?.workingDirectory
?: tryResolvePath(uvSdk?.associatedModulePath)
}
@ApiStatus.Internal
@@ -66,10 +66,11 @@ internal class PyVenvSdkConfiguration : PyProjectSdkConfigurationExtension {
getVirtualEnv(venvsInModule)?.refreshAndFindVirtualFile()
} ?: return PyResult.failure(MessageError(PyBundle.message("sdk.cannot.find.venv.for.module")))
val additionalData = createVenvAdditionalData(module).getOr { return it }
val sdk = withContext(Dispatchers.IO) {
createSdk(
PathHolder.Eel(pythonBinary.toNioPath()),
createVenvAdditionalData(),
additionalData,
null,
)
}.getOr { return it }
@@ -55,15 +55,17 @@ public final class PySdkTools {
* @return sdk
*/
public static @NotNull Sdk createTempSdk(final @NotNull VirtualFile sdkHome,
final @NotNull SdkCreationType sdkCreationType,
final @Nullable Module module,
@Nullable Disposable parentDisposable
final @NotNull SdkCreationType sdkCreationType,
final @Nullable Module module,
@Nullable Disposable parentDisposable
)
throws InvalidSdkException {
final Ref<Sdk> ref = Ref.create();
ApplicationManager.getApplication().invokeAndWait(() -> {
// sdkHome guarantees SDK name uniqueness. SdkUtil can't do that since no current SDK are provided.
var additionalData = createVenvAdditionalData();
var additionalData = module != null
? createVenvAdditionalData(module).getSuccessOrNull()
: createVenvAdditionalData(sdkHome.toNioPath().getParent());
final Sdk sdk = SdkConfigurationUtil.setupSdk(NO_SDK, sdkHome, PythonSdkType.getInstance(), additionalData, sdkHome.getPath());
Assert.assertNotNull("Failed to create SDK on " + sdkHome, sdk);
@@ -6,6 +6,7 @@ import com.intellij.codeInspection.ex.InspectionProfileImpl
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.ProjectJdkTable
@@ -53,17 +54,17 @@ import com.jetbrains.python.sdk.poetry.PyPoetrySdkAdditionalData
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.setAssociationToModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import kotlinx.coroutines.withContext
import java.nio.file.Path as NioPath
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.copyToRecursively
import kotlin.time.Duration.Companion.minutes
import java.nio.file.Path as NioPath
/**
* Inspection tests for [com.jetbrains.python.inspections.dependencies.DependenciesPsiProvider] (migrated from JUnit3).
@@ -221,13 +222,13 @@ internal class DependenciesPsiProviderTest {
fun legacyPoetryDependencies() =
genericProviderTest(
requirementsProviderType = RequirementsProviderType.PYPROJECT_TOML,
additionalData = PyPoetrySdkAdditionalData(null)
additionalData = { module -> PyPoetrySdkAdditionalData(createVenvAdditionalData(module).getOrThrow().workingDirectory) }
)
private fun runTest(
requirementsProviderType: RequirementsProviderType,
packageManagerProvider: TestPackageManagerProvider = TestPackageManagerProvider(),
additionalData: PythonSdkAdditionalData = createVenvAdditionalData(),
additionalData: (Module) -> PythonSdkAdditionalData = { createVenvAdditionalData(it).getOrThrow() },
beforePackageManager: () -> Unit = {},
body: suspend () -> Unit,
) {
@@ -243,7 +244,7 @@ internal class DependenciesPsiProviderTest {
val module = moduleFixture.get()
val venvDir = tempPathFixture.get().resolve(".venv")
val venvPython = createVenv(env.pythonPath, venvDir).getOrThrow()
createSdk(PathHolder.Eel(venvPython), additionalData)
createSdk(PathHolder.Eel(venvPython), additionalData(module))
.orThrow()
.also {
module.pythonSdk = it
@@ -272,7 +273,7 @@ internal class DependenciesPsiProviderTest {
private fun genericProviderTest(
requirementsProviderType: RequirementsProviderType,
additionalData: PythonSdkAdditionalData = createVenvAdditionalData(),
additionalData: (Module) -> PythonSdkAdditionalData = { createVenvAdditionalData(it).getOrThrow() },
) =
runTest(
requirementsProviderType = requirementsProviderType,
@@ -25,7 +25,8 @@ class PyVenvCreationManuallyShowCaseTest {
@Test
fun createVenvTest(@PythonBinaryPath python: PythonBinary, @TempDir venvDir: Directory): Unit = timeoutRunBlocking(5.minutes) {
val venvPython = createVenv(python, venvDir).getOrThrow()
val sdk = createSdk(PathHolder.Eel(venvPython), createVenvAdditionalData()).getOrThrow()
val additionalData = createVenvAdditionalData(venvDir.parent)
val sdk = createSdk(PathHolder.Eel(venvPython), additionalData).getOrThrow()
val flavorAndData = sdk.pySdkAdditionalData.flavorAndData
assertTrue(flavorAndData.sdkSeemsValid(sdk, null),
"Sdk not valid after creation")