PY-86605: Do not access IO from interpreterType.

This property violates kotlin ideology: properties must be fast, `O(1)`, and thread agnostic.

This one is used all over the system including plugins (unfortunately) and we can't force callers to use BG. So we have to make it thread-agnostic. We sacrifice accuracy in favor of speed. Most people use `.pyenv` as a dir name, and those who do not, will be excluded from the statistics. Note, that this function was never intended to be used for anything but statistics.

`LOG.error` throws exception in tests (but not in prod) so we use it to make sure we do not access IO from this function.


Merge-request: IJ-MR-191539
Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com>

GitOrigin-RevId: a92a233bcce22d47e522034c04ba2c93d2efb33b
This commit is contained in:
Ilya Kazakevich
2026-02-13 15:15:05 +00:00
committed by intellij-monorepo-bot
parent 5d1923b07d
commit bb1ccaf504
3 changed files with 25 additions and 4 deletions
@@ -54,6 +54,7 @@ import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.Path
import kotlin.io.path.copyToRecursively
import kotlin.io.path.createDirectory
import kotlin.io.path.exists
@JvmOverloads
@@ -71,8 +72,8 @@ fun testNameFixture(lowerCaseFirstLetter: Boolean = true): TestFixture<String> =
@JvmOverloads
@TestOnly
fun tempPathFixture(root: Path? = null, prefix: String = "IJ"): TestFixture<Path> = testFixture {
val tempDir = withContext(Dispatchers.IO) {
fun tempPathFixture(root: Path? = null, prefix: String = "IJ", subdirName: String? = null): TestFixture<Path> = testFixture {
var tempDir = withContext(Dispatchers.IO) {
if (root == null) {
it.eel?.fs?.createTemporaryDirectory(CreateTemporaryEntryOptions.Builder().prefix(prefix).build())?.getOrThrow()?.asNioPath()
?: Files.createTempDirectory(prefix)
@@ -84,6 +85,9 @@ fun tempPathFixture(root: Path? = null, prefix: String = "IJ"): TestFixture<Path
Files.createTempDirectory(root, prefix)
}
}
if (subdirName != null) {
tempDir = tempDir.resolve(subdirName).createDirectory()
}
val realTempDir = tempDir.toRealPath()
initialized(realTempDir) {
withContext(Dispatchers.IO) {
@@ -4,6 +4,7 @@ package com.jetbrains.python.venvReader
import com.intellij.execution.Platform
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.util.io.toCanonicalPath
import com.intellij.platform.eel.EelApi
@@ -13,6 +14,7 @@ import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.eel.provider.osFamily
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.ui.EDT
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.PythonHomePath
import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.Instance
@@ -71,7 +73,7 @@ class VirtualEnvReader private constructor(
@RequiresBackgroundThread
fun getPyenvRootDir(eel: EelApi? = getLocalEelIfApp()): Directory {
return resolveDirFromEnvOrElseGetDirInHomePath(eel, "PYENV_ROOT", ".pyenv")
return resolveDirFromEnvOrElseGetDirInHomePath(eel, "PYENV_ROOT", PYENV_DEFAULT_DIR_NAME)
}
@RequiresBackgroundThread
@@ -235,6 +237,12 @@ class VirtualEnvReader private constructor(
@RequiresBackgroundThread
private fun resolveDirFromEnvOrElseGetDirInHomePath(eel: EelApi?, env: String, dirName: String): Path {
if (EDT.isCurrentThreadEdt()) {
// This check should have been done by @RequiresBackgroundThread
// But since Kotlin doesn't support it, we have to do that imperatively.
// This error doesn't break user flow but tests
logger.error("Access from EDT isn't allowed", Throwable())
}
val envs = forcedVars
?: eel?.let { eel -> runBlockingMaybeCancellable { eel.exec.environmentVariables().eelIt().await() } }
?: System.getenv()
@@ -245,6 +253,7 @@ class VirtualEnvReader private constructor(
companion object {
private val logger = fileLogger()
internal val Instance: VirtualEnvReader = VirtualEnvReader(forcedVars = null, forcedOs = null)
@@ -259,6 +268,8 @@ class VirtualEnvReader private constructor(
@Suppress("VENV_IS_OK") // The only place it should be used in prod
const val DEFAULT_VIRTUALENV_DIRNAME: String = ".venv"
const val PYENV_DEFAULT_DIR_NAME: String = ".pyenv"
private val POSIX_PYTHON_PATTERN = Regex("^(pypy|python)(\\d+(\\.\\d+)*)?$")
private val WIN_PYTHON_PATTERN = Regex("^(pypy|python)(\\d+(\\.\\d+)*)?\\.exe$", RegexOption.IGNORE_CASE)
private fun getLocalEelIfApp(): EelApi? = if (ApplicationManager.getApplication() != null) localEel else null
@@ -182,6 +182,11 @@ val Sdk.executionType: InterpreterTarget
else -> LOCAL
}
/**
* This method is for statistics only and might be somewhat inaccurate.
* Moreover, interpreters will become a pluggable system soon, so there will be no predefined enum.
* Please, do not use it.
*/
val Sdk.interpreterType: InterpreterType
get() = when {
// The order of checks is important here since e.g. a pipenv is a virtualenv
@@ -190,7 +195,7 @@ val Sdk.interpreterType: InterpreterType
isPoetry -> POETRY
isHatch -> HATCH
this.isCondaVirtualEnv || this.sdkAdditionalData.asSafely<PythonSdkAdditionalData>()?.flavor is CondaEnvSdkFlavor -> CONDAVENV
VirtualEnvReader().isPyenvSdk(getHomePath()) -> PYENV
homePath?.contains(PYENV_PATTERN) == true -> PYENV
this.isVirtualEnv -> VIRTUALENV
else -> REGULAR
}
@@ -213,3 +218,4 @@ private val PyTargetAwareAdditionalData.executionType: InterpreterTarget
}
} ?: REMOTE_UNKNOWN
private val PYENV_PATTERN = Regex("[/\\\\]${VirtualEnvReader.PYENV_DEFAULT_DIR_NAME}[/\\\\]")