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")