From d9505303c4d42e118ec333cf909c75752813ee2d Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Fri, 17 Jul 2026 11:48:34 +0200 Subject: [PATCH] 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 --- .../sdk/poetry/PyPoetrySdkAdditionalData.kt | 9 ++-- .../python/sdk/poetry/PyPoetrySdkFlavor.kt | 10 ++++ .../resources/messages/PySdkBundle.properties | 1 + .../jetbrains/python/sdk/add/v2/FileSystem.kt | 3 +- .../target/PyTargetAwareAdditionalData.kt | 35 ++++++++++--- .../intellij/python/test/env/common/sdk.kt | 3 +- .../env/core/CachingPyEnvironmentFactory.kt | 2 +- .../env/core/DefaultPyEnvironmentFactory.kt | 2 +- .../test/env/core/PyEnvironmentFactory.kt | 3 ++ .../python/test/env/junit5/pyVenvFixture.kt | 46 +++++++++-------- .../python/sdk/uv/UvSdkAdditionalData.kt | 49 +++++++++---------- .../jetbrains/python/sdk/uv/UvSdkFlavor.kt | 25 +++++++++- .../python/sdk/uv/UvSdkFlavorData.kt | 13 ++++- .../src/com/intellij/python/venv/venv.kt | 21 ++++++-- .../PyPipfileSdkConfiguration.kt | 2 +- .../configuration/PyPoetrySdkConfiguration.kt | 2 +- .../projectCreation/venvWithSdkCreator.kt | 7 +-- .../jetbrains/python/sdk/add/v2/FileSystem.kt | 24 ++++++--- .../v2/uv/UvExistingEnvironmentSelector.kt | 16 ++---- .../python/sdk/conda/PyAddCondaTools.kt | 2 +- .../sdk/pipenv/PipenvCommandExecutor.kt | 2 +- .../sdk/pipenv/PyPipEnvSdkAdditionalData.kt | 15 ++---- .../sdk/poetry/PoetryCommandExecutor.kt | 8 +-- .../src/com/jetbrains/python/sdk/uv/UvExt.kt | 20 ++++---- .../python/sdk/uv/run/UvRunConfiguration.kt | 10 ++-- .../configuration/PyVenvSdkConfiguration.kt | 3 +- .../python/tools/sdkTools/PySdkTools.java | 10 ++-- .../packaging/DependenciesPsiProviderTest.kt | 13 ++--- .../PyVenvCreationManuallyShowCaseTest.kt | 3 +- 29 files changed, 222 insertions(+), 137 deletions(-) diff --git a/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkAdditionalData.kt b/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkAdditionalData.kt index aedebdeff398..5a6ece55e378 100644 --- a/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkAdditionalData.kt +++ b/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkAdditionalData.kt @@ -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) } } diff --git a/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkFlavor.kt b/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkFlavor.kt index b3696d34de5e..6d0d55d961a6 100644 --- a/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkFlavor.kt +++ b/python/python-poetry/backend/src/com/jetbrains/python/sdk/poetry/PyPoetrySdkFlavor.kt @@ -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() { override fun getIcon(): Icon = PythonCommunityImplPoetryCommonIcons.Poetry override fun getFlavorDataClass(): Class = PyFlavorData.Empty::class.java + override fun migrateAdditionalData( + additionalData: PythonSdkAdditionalData, + data: PyFlavorData.Empty, + ): AdditionalDataMigration { + val workingDirectory = additionalData.associatedModulePath?.takeIf { it.isNotBlank() }?.toNioPathOrNull() + return AdditionalDataMigration(data, workingDirectory) + } + override fun isValidSdkPath(pythonBinaryPath: Path): Boolean = false } diff --git a/python/python-sdk/resources/messages/PySdkBundle.properties b/python/python-sdk/resources/messages/PySdkBundle.properties index fa32585817ff..d9eb4680ac9f 100644 --- a/python/python-sdk/resources/messages/PySdkBundle.properties +++ b/python/python-sdk/resources/messages/PySdkBundle.properties @@ -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, diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/python-sdk/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 089a05af324a..bc52cd3adf85 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -26,6 +26,7 @@ interface FileSystem

{ 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

{ suspend fun detectSelectableVenv(projectPathPrefix: Path): List> 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 diff --git a/python/python-sdk/src/com/jetbrains/python/target/PyTargetAwareAdditionalData.kt b/python/python-sdk/src/com/jetbrains/python/target/PyTargetAwareAdditionalData.kt index 9ad81ad253c7..36174d9dd7f2 100644 --- a/python/python-sdk/src/com/jetbrains/python/target/PyTargetAwareAdditionalData.kt +++ b/python/python-sdk/src/com/jetbrains/python/target/PyTargetAwareAdditionalData.kt @@ -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() diff --git a/python/python-test-env/common/src/com/intellij/python/test/env/common/sdk.kt b/python/python-test-env/common/src/com/intellij/python/test/env/common/sdk.kt index 157a6e0e3288..c600132fb73b 100644 --- a/python/python-test-env/common/src/com/intellij/python/test/env/common/sdk.kt +++ b/python/python-test-env/common/src/com/intellij/python/test/env/common/sdk.kt @@ -33,8 +33,7 @@ suspend fun PyEnvironmentFactory.createSdk(request: SdkCreationRequest): Pair { - 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 { diff --git a/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt b/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt index bfc526ee4ef8..ebff27daa71b 100644 --- a/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt +++ b/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt @@ -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() diff --git a/python/python-test-env/core/src/com/intellij/python/test/env/core/DefaultPyEnvironmentFactory.kt b/python/python-test-env/core/src/com/intellij/python/test/env/core/DefaultPyEnvironmentFactory.kt index 9f2ffca5cc8b..c88d4c1f6357 100644 --- a/python/python-test-env/core/src/com/intellij/python/test/env/core/DefaultPyEnvironmentFactory.kt +++ b/python/python-test-env/core/src/com/intellij/python/test/env/core/DefaultPyEnvironmentFactory.kt @@ -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) -> Boolean, PyEnvironmentProvider<*>>> ): PyEnvironmentFactory { diff --git a/python/python-test-env/core/src/com/intellij/python/test/env/core/PyEnvironmentFactory.kt b/python/python-test-env/core/src/com/intellij/python/test/env/core/PyEnvironmentFactory.kt index 5f96f1ae5225..aa5d8a97c629 100644 --- a/python/python-test-env/core/src/com/intellij/python/test/env/core/PyEnvironmentFactory.kt +++ b/python/python-test-env/core/src/com/intellij/python/test/env/core/PyEnvironmentFactory.kt @@ -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) } diff --git a/python/python-test-env/junit5/src/com/intellij/python/test/env/junit5/pyVenvFixture.kt b/python/python-test-env/junit5/src/com/intellij/python/test/env/junit5/pyVenvFixture.kt index dc9cea1137c9..4c72fdb46d89 100644 --- a/python/python-test-env/junit5/src/com/intellij/python/test/env/junit5/pyVenvFixture.kt +++ b/python/python-test-env/junit5/src/com/intellij/python/test/env/junit5/pyVenvFixture.kt @@ -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>.pyVenvFixture( - where: TestFixture, - addToSdkTable: Boolean, - moduleFixture: TestFixture? = null, + where: TestFixture, + addToSdkTable: Boolean, + moduleFixture: TestFixture? = null, ): TestFixture = 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) + } + } + } } \ No newline at end of file diff --git a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt index f6fd4438363d..d915a0ce8c6a 100644 --- a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt +++ b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt @@ -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) - } } } \ No newline at end of file diff --git a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavor.kt b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavor.kt index ec81943ac5b0..5b03d497aad9 100644 --- a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavor.kt +++ b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavor.kt @@ -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() { +@ApiStatus.Internal +object UvSdkFlavor : CPythonSdkFlavor() { override fun getIcon(): Icon = PythonUvCommonIcons.UV override fun getFlavorDataClass(): Class = UvSdkFlavorData::class.java + override fun migrateAdditionalData( + additionalData: PythonSdkAdditionalData, + data: UvSdkFlavorData, + ): AdditionalDataMigration { + 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 } diff --git a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavorData.kt b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavorData.kt index 862a6da28e88..81a6ca930637 100644 --- a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavorData.kt +++ b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkFlavorData.kt @@ -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) } \ No newline at end of file diff --git a/python/python-venv/src/com/intellij/python/venv/venv.kt b/python/python-venv/src/com/intellij/python/venv/venv.kt index c1e1870778ba..e5147712bbfe 100644 --- a/python/python-venv/src/com/intellij/python/venv/venv.kt +++ b/python/python-venv/src/com/intellij/python/venv/venv.kt @@ -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())) \ No newline at end of file +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 { + return ModuleOrProject.ModuleAndProject(module).workingDirectory?.let { + PyResult.success(createVenvAdditionalData(it)) + } ?: PyResult.localizedError(PySdkBundle.message("python.sdk.cannot.create.working.directory.empty")) +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/pipenv/sdk/configuration/PyPipfileSdkConfiguration.kt b/python/src/com/jetbrains/python/pipenv/sdk/configuration/PyPipfileSdkConfiguration.kt index 22705f0dbed1..1bad1c3489c3 100644 --- a/python/src/com/jetbrains/python/pipenv/sdk/configuration/PyPipfileSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/pipenv/sdk/configuration/PyPipfileSdkConfiguration.kt @@ -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 } diff --git a/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt b/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt index acf307bfdf2a..4552e12da4cb 100644 --- a/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt +++ b/python/src/com/jetbrains/python/poetry/sdk/configuration/PyPoetrySdkConfiguration.kt @@ -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 } diff --git a/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt b/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt index ff40e339a349..629671eaa3f8 100644 --- a/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt +++ b/python/src/com/jetbrains/python/projectCreation/venvWithSdkCreator.kt @@ -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 = +private suspend fun getSdk(pythonPath: PythonBinary, module: Module): PyResult = 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) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index b724839ae435..538c29755a39 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -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 { + 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 { + 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? { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt index 7271e92e1e07..cc77ce22c9c9 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt @@ -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

(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

(model: PythonMutabl } } } - - private fun extractModule(moduleOrProject: ModuleOrProject): Module? = - when (moduleOrProject) { - is ModuleOrProject.ModuleAndProject -> moduleOrProject.module - else -> null - } } diff --git a/python/src/com/jetbrains/python/sdk/conda/PyAddCondaTools.kt b/python/src/com/jetbrains/python/sdk/conda/PyAddCondaTools.kt index 4470c8173f21..22014035692a 100644 --- a/python/src/com/jetbrains/python/sdk/conda/PyAddCondaTools.kt +++ b/python/src/com/jetbrains/python/sdk/conda/PyAddCondaTools.kt @@ -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) diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt index 61c546c92c3e..9d192282045c 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt @@ -84,7 +84,7 @@ internal suspend fun setupPipEnvSdkWithProgressReport( return createSdk( pythonBinaryPath = PathHolder.Eel(pythonExecutablePath), - sdkAdditionalData = PyPipEnvSdkAdditionalData() + sdkAdditionalData = PyPipEnvSdkAdditionalData(moduleBasePath) ) } diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkAdditionalData.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkAdditionalData.kt index 458b60203a07..80ed36b66a90 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkAdditionalData.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkAdditionalData.kt @@ -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) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt index a362622dcc83..93b5061ab59a 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt @@ -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 */ -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 { - 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 { diff --git a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt index a55beed95848..fc0ed31c7a42 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt @@ -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

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

setupExistingEnvAndSdk( fileSystem: FileSystem

, usePip: Boolean, ): PyResult = 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 } diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt index 280d87e59055..b71147e627b5 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt @@ -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 = 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 diff --git a/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt b/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt index 1746839603d4..7c8922449fc9 100644 --- a/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt @@ -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 } diff --git a/python/testFramework/src/com/jetbrains/python/tools/sdkTools/PySdkTools.java b/python/testFramework/src/com/jetbrains/python/tools/sdkTools/PySdkTools.java index 39e392c54e60..71587157e84d 100644 --- a/python/testFramework/src/com/jetbrains/python/tools/sdkTools/PySdkTools.java +++ b/python/testFramework/src/com/jetbrains/python/tools/sdkTools/PySdkTools.java @@ -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 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); diff --git a/python/testSrc/com/intellij/python/junit5Tests/env/packaging/DependenciesPsiProviderTest.kt b/python/testSrc/com/intellij/python/junit5Tests/env/packaging/DependenciesPsiProviderTest.kt index 4734f4d2303e..9174980892fa 100644 --- a/python/testSrc/com/intellij/python/junit5Tests/env/packaging/DependenciesPsiProviderTest.kt +++ b/python/testSrc/com/intellij/python/junit5Tests/env/packaging/DependenciesPsiProviderTest.kt @@ -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, diff --git a/python/testSrc/com/intellij/python/junit5Tests/env/venv/showCase/PyVenvCreationManuallyShowCaseTest.kt b/python/testSrc/com/intellij/python/junit5Tests/env/venv/showCase/PyVenvCreationManuallyShowCaseTest.kt index d1aa712e5a27..ada61a777338 100644 --- a/python/testSrc/com/intellij/python/junit5Tests/env/venv/showCase/PyVenvCreationManuallyShowCaseTest.kt +++ b/python/testSrc/com/intellij/python/junit5Tests/env/venv/showCase/PyVenvCreationManuallyShowCaseTest.kt @@ -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")