PY-77226: Introduce InterpreterService.

This service is aimed to substitute various SDK-management tools in PyCharm.

Begin with `InterpreterService()` function.

GitOrigin-RevId: 368d56c4a78812fe81de941e5e5ce61a56d385e6
This commit is contained in:
Ilya.Kazakevich
2025-06-08 17:33:51 +02:00
committed by intellij-monorepo-bot
parent 87108b9813
commit a0231c334a
22 changed files with 672 additions and 2 deletions

1
.idea/modules.xml generated
View File

@@ -1059,6 +1059,7 @@
<module fileurl="file://$PROJECT_DIR$/python/installer/intellij.python.community.impl.installer.iml" filepath="$PROJECT_DIR$/python/installer/intellij.python.community.impl.installer.iml" />
<module fileurl="file://$PROJECT_DIR$/python/poetry/intellij.python.community.impl.poetry.iml" filepath="$PROJECT_DIR$/python/poetry/intellij.python.community.impl.poetry.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-venv/intellij.python.community.impl.venv.iml" filepath="$PROJECT_DIR$/python/python-venv/intellij.python.community.impl.venv.iml" />
<module fileurl="file://$PROJECT_DIR$/python/interpreters/intellij.python.community.interpreters.iml" filepath="$PROJECT_DIR$/python/interpreters/intellij.python.community.interpreters.iml" />
<module fileurl="file://$PROJECT_DIR$/python/junit5Tests-framework/intellij.python.community.junit5Tests.framework.iml" filepath="$PROJECT_DIR$/python/junit5Tests-framework/intellij.python.community.junit5Tests.framework.iml" />
<module fileurl="file://$PROJECT_DIR$/python/junit5Tests-framework/conda/intellij.python.community.junit5Tests.framework.conda.iml" filepath="$PROJECT_DIR$/python/junit5Tests-framework/conda/intellij.python.community.junit5Tests.framework.conda.iml" />
<module fileurl="file://$PROJECT_DIR$/python/pluginCore/intellij.python.community.plugin.iml" filepath="$PROJECT_DIR$/python/pluginCore/intellij.python.community.plugin.iml" />

View File

@@ -291,6 +291,7 @@ jvm_library(
"@lib//:kaml",
"//python/impl.helperLocator:community-helpersLocator",
"//python/python-exec-service/execService.python",
"//python/interpreters",
],
exports = [
"//python/openapi:community",

View File

@@ -188,5 +188,6 @@
<orderEntry type="library" name="kaml" level="project" />
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
<orderEntry type="module" module-name="intellij.python.community.interpreters" />
</component>
</module>

View File

@@ -0,0 +1,81 @@
### auto-generated section `build intellij.python.community.interpreters` start
load("@community//build:tests-options.bzl", "jps_test")
load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_resources", "jvm_test")
jvm_resources(
name = "interpreters_resources",
files = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "interpreters",
module_name = "intellij.python.community.interpreters",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java"], allow_empty = True),
deps = [
"@lib//:kotlin-stdlib",
"//python/services/shared",
"@lib//:jetbrains-annotations",
"//platform/extensions",
"//python/python-sdk:sdk",
"//python/python-psi-impl:psi-impl",
"//platform/projectModel-api:projectModel",
"//platform/util",
"//platform/diagnostic",
"//python/python-exec-service/execService.python",
"//python/python-exec-service:community-execService",
"//python/python-parser:parser",
"//python/openapi:community",
"//platform/core-api:core",
"//platform/eel-provider",
],
runtime_deps = [":interpreters_resources"]
)
jvm_library(
name = "interpreters_test_lib",
visibility = ["//visibility:public"],
srcs = glob(["tests/**/*.kt", "tests/**/*.java"], allow_empty = True),
associates = [":interpreters"],
deps = [
"@lib//:kotlin-stdlib",
"//python/services/shared",
"//python/services/shared:shared_test_lib",
"@lib//:jetbrains-annotations",
"//platform/extensions",
"//python/python-sdk:sdk",
"//python/python-sdk:sdk_test_lib",
"//python/python-psi-impl:psi-impl",
"//platform/projectModel-api:projectModel",
"//platform/util",
"//platform/diagnostic",
"//python/python-exec-service/execService.python",
"//python/python-exec-service/execService.python:execService.python_test_lib",
"//python/python-exec-service:community-execService",
"//python/python-exec-service:community-execService_test_lib",
"//python/python-parser:parser",
"//python/openapi:community",
"//python/openapi:community_test_lib",
"//platform/core-api:core",
"//platform/eel-provider",
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
"@lib//:junit5",
"@lib//:junit5Params",
"//platform/testFramework/junit5",
"//platform/testFramework/junit5:junit5_test_lib",
"//platform/lang-core",
"//python/pluginCore:community-plugin",
"//python/python-venv:community-impl-venv",
"//python/python-venv:community-impl-venv_test_lib",
"//platform/execution",
"//python/impl.helperLocator:community-helpersLocator",
],
runtime_deps = [":interpreters_resources"]
)
jps_test(
name = "interpreters_test",
runtime_deps = [":interpreters_test_lib"]
)
### auto-generated section `build intellij.python.community.interpreters` end

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.python.community.interpreters" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.python.community.services.shared" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.extensions" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.psi.impl" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.diagnostic" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
<orderEntry type="module" module-name="intellij.python.parser" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.lang.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.plugin" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.execution" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" scope="TEST" />
</component>
</module>

View File

@@ -0,0 +1,20 @@
<idea-plugin>
<dependencies>
<module name="intellij.python.community.services.shared"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.psi.impl"/>
<module name="intellij.python.community.execService.python"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.parser"/>
<module name="intellij.python.community"/>
</dependencies>
<extensionPoints>
<extensionPoint qualifiedName="Pythonid.interpreterProvider"
dynamic="true"
interface="com.intellij.python.community.interpreters.spi.InterpreterProvider"/>
</extensionPoints>
<extensions defaultExtensionNs="Pythonid">
<interpreterProvider implementation="com.intellij.python.community.interpreters.impl.VanillaInterpreterProvider"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,3 @@
py.interpreter.broken=Interpreter Home path {0} is invalid: {1}
py.interpreter.no.version=Interpreter failed to report its version {0}
py.unknown.type=Unknown interpreter type {0}. Plugin missing?

View File

@@ -0,0 +1,62 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.interpreters.advancedApi
import com.intellij.python.community.execService.*
import com.intellij.python.community.execService.python.HelperName
import com.intellij.python.community.execService.python.advancedApi.executeHelperAdvanced
import com.intellij.python.community.execService.python.advancedApi.executePythonAdvanced
import com.intellij.python.community.execService.python.advancedApi.validatePythonAndGetVersion
import com.intellij.python.community.interpreters.ValidInterpreter
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.psi.LanguageLevel
import org.jetbrains.annotations.ApiStatus
// This in advanced API, most probably you need "api.kt"
/**
* Execute [python]
*/
suspend fun <T> ExecService.executePythonAdvanced(
python: ValidInterpreter,
argsBuilder: suspend ArgsBuilder.() -> Unit = {},
options: ExecOptions = ExecOptions(),
processInteractiveHandler: ProcessInteractiveHandler<T>,
): PyResult<T> =
executePythonAdvanced(python.asExecutablePython, argsBuilder, options, processInteractiveHandler)
/**
* Execute [helper] on [python]. For remote eels, [helper] is copied (but only one file!).
*/
suspend fun <T> ExecService.executeHelperAdvanced(
python: ValidInterpreter,
helper: HelperName,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
processOutputTransformer: ProcessOutputTransformer<T>,
): PyResult<T> = executeHelperAdvanced(python.asExecutablePython, helper, args, options, procListener, processOutputTransformer)
/**
* Ensures that this python is executable and returns its version. Error if python is broken.
*
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
* As we need workable pythons, we validate it by executing
*/
@ApiStatus.Internal
suspend fun ValidInterpreter.validatePythonAndGetVersion(): PyResult<LanguageLevel> =
ExecService().validatePythonAndGetVersion(asExecutablePython)
/**
* Execute [helper] on [python]. For remote eels, [helper] is copied (but only one file!).
* Returns `stdout`
*/
suspend fun ExecService.executeHelper(
python: ValidInterpreter,
helper: HelperName,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyResult<String> =
executeHelperAdvanced(python.asExecutablePython, helper, args, options, procListener, ZeroCodeStdoutTransformer)

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.python.community.interpreters.advancedApi;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,94 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.interpreters
import com.intellij.openapi.module.Module
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.PyProcessListener
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.intellij.python.community.execService.impl.transformerToHandler
import com.intellij.python.community.execService.python.HelperName
import com.intellij.python.community.execService.python.advancedApi.executeHelperAdvanced
import com.intellij.python.community.execService.python.advancedApi.executePythonAdvanced
import com.intellij.python.community.interpreters.impl.InterpreterFields
import com.intellij.python.community.interpreters.impl.InterpreterServiceImpl
import com.intellij.python.community.services.shared.PythonWithLanguageLevel
import com.intellij.python.community.services.shared.PythonWithUi
import com.jetbrains.python.errorProcessing.PyResult
import org.jetbrains.annotations.Nls
import java.nio.file.Path
/**
* Python interpreter can be either valid or invalid (broken at the moment it was loaded)
*/
sealed interface Interpreter : InterpreterFields
/**
* Interpreter was usable at the moment it was loaded.
* It has [ui] and [getReadableName] and can be used to execute code against [ExecService] (see extension functions)
*/
interface ValidInterpreter : PythonWithLanguageLevel, PythonWithUi, Interpreter
/**
* At the moment of loading this interpreter was invalid due to [invalidMessage].
*/
interface InvalidInterpreter : Interpreter {
val invalidMessage: @Nls String
}
/**
* Obtain it by means of eponymous function.
*/
interface InterpreterService {
/**
* Which interpreters might be used for this directory?
*/
suspend fun getInterpreters(projectDir: Path): List<Interpreter>
/**
* Is there an [Interpreter] associated with this [module]?
*/
suspend fun getForModule(module: Module): Interpreter?
}
/***
* ```kotlin
* InterpreterService().getInterpreters(path)
* ```
*/
fun InterpreterService(): InterpreterService = InterpreterServiceImpl
suspend fun InterpreterService.getValidInterpreters(projectDir: Path): List<ValidInterpreter> =
getInterpreters(projectDir).mapNotNull {
when (it) {
is InvalidInterpreter -> null
is ValidInterpreter -> it
}
}
/**
* Execute [helper] on [python]. For remote eels, [helper] is copied (but only one file!).
* Returns `stdout`
*/
suspend fun ExecService.executeHelper(
python: ValidInterpreter,
helper: HelperName,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyResult<String> =
executeHelperAdvanced(python.asExecutablePython, helper, args, options, procListener, ZeroCodeStdoutTransformer)
/**
* Execute command on [python].
* Returns `stdout`
*/
suspend fun ExecService.executeGetStdout(
python: ValidInterpreter,
args: List<String> = emptyList(),
options: ExecOptions = ExecOptions(),
procListener: PyProcessListener? = null,
): PyResult<String> =
executePythonAdvanced(python.asExecutablePython, { addArgs(*args.toTypedArray()) }, options, transformerToHandler(procListener, ZeroCodeStdoutTransformer))

View File

@@ -0,0 +1,60 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.interpreters.impl
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.community.execService.python.advancedApi.ExecutablePython
import com.intellij.python.community.interpreters.Interpreter
import com.intellij.python.community.interpreters.InvalidInterpreter
import com.intellij.python.community.interpreters.ValidInterpreter
import com.intellij.python.community.services.shared.PythonWithName
import com.intellij.python.community.services.shared.UICustomization
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.util.*
internal class ValidInterpreterImpl(
override val languageLevel: LanguageLevel,
override val asExecutablePython: ExecutablePython,
private val mixin: SdkMixin,
override val ui: UICustomization?,
) : ValidInterpreter, InterpreterFields by mixin {
override fun toString(): String {
return "ValidInterpreterImpl(languageLevel=$languageLevel, asExecutablePython=$asExecutablePython)"
}
}
internal class InvalidInterpreterImpl(
private val mixin: SdkMixin,
override val invalidMessage: @Nls String,
) : InvalidInterpreter, InterpreterFields by mixin {
override fun toString(): String {
return "InvalidInterpreterImpl(invalidMessage='$invalidMessage')"
}
}
@ApiStatus.NonExtendable
interface InterpreterFields : PythonWithName {
/**
* Currently, interpreters are based on SDK. But this is an implementation detail to be changed soon.
* Do not use this field unless absolutely necessary.
*/
val sdk: Sdk
val id: UUID
}
internal class SdkMixin(override val sdk: Sdk, data: PythonSdkAdditionalData) : InterpreterFields {
override val id: UUID = data.uuid
override suspend fun getReadableName(): @Nls String = sdk.name
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Interpreter) return false
return id == other.id
}
override fun hashCode(): Int = id.hashCode()
}

View File

@@ -0,0 +1,110 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.interpreters.impl
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.python.advancedApi.ExecutablePython
import com.intellij.python.community.execService.python.advancedApi.validatePythonAndGetVersion
import com.intellij.python.community.interpreters.Interpreter
import com.intellij.python.community.interpreters.InterpreterService
import com.intellij.python.community.interpreters.impl.PyInterpreterBundle.message
import com.intellij.python.community.interpreters.spi.InterpreterProvider
import com.intellij.python.community.services.shared.UICustomization
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.Path
internal object InterpreterServiceImpl : InterpreterService {
private val logger = fileLogger()
override suspend fun getInterpreters(projectDir: Path): List<Interpreter> {
val sdkAndDatas = PythonSdkUtil.getAllSdks()
.map { Pair(it.getOrCreateAdditionalData(), it) }
.filter { (data, _) -> sdkApplicableToThePath(data, projectDir) }
val result = mutableListOf<Interpreter>()
for ((additionalData, sdk) in sdkAndDatas) {
val interpreter = findInterpreter(additionalData, sdk)
result.add(interpreter)
}
return result
}
override suspend fun getForModule(module: Module): Interpreter? {
val sdk = ModuleRootManager.getInstance(module).sdk ?: return null
if (sdk.sdkAdditionalData !is PythonSdkAdditionalData) {
return null
}
return findInterpreter(sdk.getOrCreateAdditionalData(), sdk)
}
private suspend fun findInterpreter(
additionalData: PythonSdkAdditionalData,
sdk: Sdk,
): Interpreter {
val flavorData = additionalData.flavorAndData.data
val provider = InterpreterProvider.providerForData(flavorData)
val interpreter = if (provider == null) {
InvalidInterpreterImpl(SdkMixin(sdk, additionalData), message("py.unknown.type", flavorData.javaClass))
}
else {
createInterpreter(provider, sdk, additionalData, flavorData)
}
return interpreter
}
private fun sdkApplicableToThePath(data: PythonSdkAdditionalData, projectDir: Path): Boolean {
val associatedPathStr = data.associatedModulePath ?: return true
val associatedPath = try {
Path(associatedPathStr)
}
catch (e: InvalidPathException) {
logger.warn("Skipping broken sdk associated with $associatedPathStr", e)
return false
}
return projectDir.startsWith(associatedPath)
}
}
private suspend fun <T : PyFlavorData> createInterpreter(provider: InterpreterProvider<T>, sdk: Sdk, additionalData: PythonSdkAdditionalData, flavorData: T): Interpreter {
val homePath = try {
Path(sdk.homePath!!)
}
catch (e: InvalidPathException) {
return InvalidInterpreterImpl(SdkMixin(sdk, additionalData), message("py.interpreter.broken", sdk.homePath!!, e.localizedMessage))
}
when (val r = provider.createExecutablePython(homePath, flavorData)) {
is Result.Failure -> {
return InvalidInterpreterImpl(SdkMixin(sdk, additionalData), r.error.message)
}
is Result.Success -> {
val executablePython = r.result
val languageLevel = sdk.versionString?.let { LanguageLevel.fromPythonVersionSafe(it) }
?: ExecService().validatePythonAndGetVersion(executablePython).getOr {
return InvalidInterpreterImpl(SdkMixin(sdk, additionalData), message("py.interpreter.no.version", it.error.message))
}
return ValidInterpreterImpl(languageLevel, executablePython, SdkMixin(sdk, additionalData), provider.ui)
}
}
}
private class VanillaInterpreterProvider : InterpreterProvider<PyFlavorData.Empty> {
override val ui: UICustomization? = null
override val flavorDataClass: Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
override suspend fun createExecutablePython(sdkHomePath: Path, flavorData: PyFlavorData.Empty): Result<ExecutablePython, MessageError> {
return Result.success(ExecutablePython.vanillaExecutablePython(sdkHomePath))
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.interpreters.impl
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
import java.util.function.Supplier
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
internal object PyInterpreterBundle {
private const val BUNDLE_FQN: @NonNls String = "messages.PyInterpreterBundle"
private val BUNDLE = DynamicBundle(PyInterpreterBundle::class.java, BUNDLE_FQN)
fun message(
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
vararg params: Any,
): @Nls String {
return BUNDLE.getMessage(key, *params)
}
fun messagePointer(
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
vararg params: Any,
): Supplier<String> {
return BUNDLE.getLazyMessage(key, *params)
}
}

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.python.community.interpreters.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* API for python interpreters to be used instead of SDK, see `api.kt`
* Entry point is {@link com.intellij.python.community.interpreters.InterpreterService}
*/
@ApiStatus.Internal
package com.intellij.python.community.interpreters;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,32 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.community.interpreters.spi
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.community.execService.python.advancedApi.ExecutablePython
import com.intellij.python.community.interpreters.Interpreter
import com.intellij.python.community.services.shared.UICustomization
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.sdk.flavors.PyFlavorData
import java.nio.file.Path
/**
* Bridge between [Sdk] and [Interpreter].
* Each [Sdk] has [PyFlavorData].
*
* [InterpreterProvider] must be found by [PyFlavorData] to convert [Interpreter]
*/
interface InterpreterProvider<T : PyFlavorData> {
companion object {
private val EP: ExtensionPointName<InterpreterProvider<*>> = ExtensionPointName<InterpreterProvider<*>>("Pythonid.interpreterProvider")
@Suppress("UNCHECKED_CAST")
fun <T : PyFlavorData> providerForData(data: T): InterpreterProvider<T>? =
EP.extensionList.firstOrNull { it.flavorDataClass.isInstance(data) } as InterpreterProvider<T>?
}
val ui: UICustomization?
val flavorDataClass: Class<T>
suspend fun createExecutablePython(sdkHomePath: Path, flavorData: T): Result<ExecutablePython, MessageError>
}

View File

@@ -0,0 +1,8 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* To be used by PyCharm exec team only
*/
@ApiStatus.Internal
package com.intellij.python.community.interpreters.spi;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,96 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.junit5Tests.env.tests.interpreters
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
import com.intellij.python.community.impl.venv.tests.pyVenvFixture
import com.intellij.python.community.interpreters.*
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.python.junit5Tests.framework.env.pySdkFixture
import com.intellij.testFramework.junit5.fixture.TestFixture
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
import com.intellij.testFramework.junit5.fixture.tempPathFixture
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.setAssociationToPath
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import java.util.*
import kotlin.io.path.deleteIfExists
import kotlin.io.path.pathString
import kotlin.io.path.writeText
@PyEnvTestCase
class InterpreterServiceShowCaseTest {
private val sdkFixture = pySdkFixture()
private val validSdkFixture = sdkFixture.pyVenvFixture(tempPathFixture(prefix = "valid"), addToSdkTable = true)
private val validSdkFixture2 = sdkFixture.pyVenvFixture(tempPathFixture(prefix = "valid2"), addToSdkTable = true)
private val invalidSdkFixture = sdkFixture.pyVenvFixture(tempPathFixture(prefix = "invalid"), addToSdkTable = true)
private val sdkFixtureAnotherPath = sdkFixture.pyVenvFixture(tempPathFixture(prefix = "anotherpath"), addToSdkTable = true)
private val moduleFixture = projectFixture().moduleFixture()
@Test
fun testListInterpreters(@TempDir dir: Path): Unit = runBlocking {
val interpreterService = InterpreterService()
edtWriteAction {
val m = invalidSdkFixture.get().sdkModificator
m.homePath += "junk"
m.commitChanges()
}
val validSdk = validSdkFixture.get()
validSdk.setAssociationToPath(null)
validSdkFixture2.get().setAssociationToPath(dir.pathString)
sdkFixtureAnotherPath.get().setAssociationToPath(dir.resolveSibling("asdasd").pathString)
val module = moduleFixture.get()
val expectedInterpreters = setOf(sdkFixture, validSdkFixture, validSdkFixture2,
invalidSdkFixture).associateBy { it.id() }.toMutableMap()
val interpreters = interpreterService.getInterpreters(dir)
for (i in interpreters) {
val sdk = expectedInterpreters.remove(i.id)?.get()
Assertions.assertTrue(sdk != null, "Unexpected interpreter: $i")
Assertions.assertEquals(sdk, i.sdk, "Wrong sdk")
ModuleRootModificationUtil.setModuleSdk(module, sdk!!)
Assertions.assertEquals(sdk.getOrCreateAdditionalData().uuid, interpreterService.getForModule(module)!!.id, "No module set")
when (i) {
is InvalidInterpreter -> {
Assertions.assertEquals(invalidSdkFixture.id(), i.id, "Unexpected invalid interpreter $i")
}
is ValidInterpreter -> {
val version = ExecService().executeGetStdout(i, listOf("--version")).orThrow().trim()
Assertions.assertTrue(version.isNotBlank(), "No version returned")
val helperName = "file.py"
val helper = PythonHelpersLocator.getCommunityHelpersRoot().resolve(helperName)
try {
helper.writeText("print(1)")
val helperOutput = ExecService().executeHelper(i, helperName).orThrow().trim()
Assertions.assertEquals("1", helperOutput, "Wrong helper output")
}
finally {
helper.deleteIfExists()
}
}
}
}
if (expectedInterpreters.isNotEmpty()) {
fail("Missing interpreters: ${expectedInterpreters.values}")
}
}
}
private fun TestFixture<Sdk>.id(): UUID = get().getOrCreateAdditionalData().uuid

View File

@@ -16,7 +16,21 @@ import org.jetbrains.annotations.ApiStatus.Internal
* Sdk with environment info. Use it as a regular sdk, but env fixtures (venv, conda) might use [env]
*/
@Internal
class SdkFixture<ENV : Any> internal constructor(private val sdk: Sdk, val env: ENV) : Sdk by sdk
class SdkFixture<ENV : Any> internal constructor(private val sdk: Sdk, val env: ENV) : Sdk by sdk {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return when (other) {
is SdkFixture<*> -> other.sdk == sdk
is Sdk -> other == sdk
else -> false
}
}
override fun hashCode(): Int {
return sdk.hashCode()
}
}
/**

View File

@@ -64,6 +64,7 @@
- name: intellij.python.community.services.shared
- name: intellij.python.community.services.internal.impl
- name: intellij.python.community.services.systemPython
- name: intellij.python.community.interpreters
- name: intellij.python.community.plugin.minor
- name: intellij.python.community.plugin.minorRider
- name: intellij.python.community.communityOnly

View File

@@ -52,7 +52,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.community.services.shared" loading="embedded"/>
<module name="intellij.python.community.services.internal.impl" loading="embedded"/>
<module name="intellij.python.community.services.systemPython" loading="embedded"/>
<module name="intellij.python.community.interpreters" loading="embedded"/>
<!--Mini-IDes support community python only-->
<module name="intellij.python.community.plugin.minor"/> <!-- Python for Mini-IDEs-->
<module name="intellij.python.community.plugin.minorRider"/> <!-- Python special support for Rider -->

View File

@@ -6,6 +6,7 @@
<module name="intellij.python.community.helpersLocator"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.community.execService.python"/>
<module name="intellij.python.community.interpreters"/>
</dependencies>
<resource-bundle>messages.PyBundle</resource-bundle>