[pycharm] PY-79448 Add version detection for lock check

GitOrigin-RevId: daaa8a38f4a0dba4113659ab536352431ca39aea
This commit is contained in:
David Lysenko
2025-03-31 19:33:31 +00:00
committed by intellij-monorepo-bot
parent 74563feb36
commit 6a7b8a67ec
@@ -32,6 +32,8 @@ import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import io.github.z4kn4fein.semver.Version
import io.github.z4kn4fein.semver.toVersion
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.SystemDependent
@@ -45,6 +47,7 @@ import kotlin.io.path.pathString
*/
private const val REPLACE_PYTHON_VERSION = """import re,sys;f=open("pyproject.toml", "r+");orig=f.read();f.seek(0);f.write(re.sub(r"(python = \"\^)[^\"]+(\")", "\g<1>"+'.'.join(str(v) for v in sys.version_info[:2])+"\g<2>", orig))"""
private val poetryNotFoundException: Throwable = Throwable(PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message"))
private val VERSION_2 = "2.0.0".toVersion()
@Internal
suspend fun runPoetry(projectPath: Path?, vararg args: String): Result<String> {
@@ -92,7 +95,7 @@ suspend fun validatePoetryExecutable(poetryExecutable: Path?): ValidationInfo? =
*/
internal suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): Result<String> {
val projectPath = sdk.associatedModulePath?.let { Path.of(it) } ?: return Result.failure(poetryNotFoundException) // Choose a correct sdk
return reportSequentialProgress(2) { reporter ->
return reportSequentialProgress(2) { reporter ->
reporter.itemStep {
runPoetry(projectPath, "env", "use", sdk.homePath!!)
}
@@ -155,7 +158,12 @@ internal suspend fun detectPoetryEnvs(module: Module?, existingSdkPaths: Set<Str
return getPoetryEnvs(path).filter { existingSdkPaths?.contains(getPythonExecutable(it)) != false }.map { PyDetectedSdk(getPythonExecutable(it)) }
}
internal suspend fun getPoetryVersion(): String? = runPoetry(null, "--version").getOrNull()?.split(' ')?.lastOrNull()
internal suspend fun getPoetryVersion(): String? =
runPoetry(null, "--version")
.getOrNull()
?.split(' ')
?.lastOrNull()
?.replace(Regex("""\D+$"""), "") // strip all non-numeric characters after the version
@Internal
suspend fun getPythonExecutable(homePath: String): String = withContext(Dispatchers.IO) {
@@ -208,10 +216,12 @@ suspend fun poetryShowOutdated(sdk: Sdk): Result<Map<String, PythonOutdatedPacka
@Internal
suspend fun poetryListPackages(sdk: Sdk): Result<Pair<List<PyPackage>, List<PyRequirement>>> {
// Just in case there were any changes to pyproject.toml
if (runPoetryWithSdk(sdk, "lock", "--check").isFailure) {
if (runPoetryWithSdk(sdk, "lock", "--no-update").isFailure) {
runPoetryWithSdk(sdk, "lock")
val version = getPoetryVersion()?.toVersion()
// Ensure that the lock file is up to date.
if (!checkLock(sdk, version)) {
fixLock(sdk, version).getOrElse {
return Result.failure(it)
}
}
@@ -224,6 +234,28 @@ suspend fun poetryListPackages(sdk: Sdk): Result<Pair<List<PyPackage>, List<PyRe
}
}
@Internal
suspend fun checkLock(sdk: Sdk, version: Version?): Boolean {
// From Poetry 1.6.0 and forward, `poetry check --lock` should be used to figure out the validity of the lock file.
// However, this command fails whenever a README file (as described in pyproject.toml) is absent, without even checking the lock file.
// The old command, albeit deprecated, doesn't check for the README; instead, it only checks for the validity of the lock file.
// After Poetry 2.0.0 and forward, `poetry check --lock` also only checks for the lock file, ignoring the existence of a README.
if (version == null || version >= VERSION_2) {
return runPoetryWithSdk(sdk, "check", "--lock").isSuccess
}
return runPoetryWithSdk(sdk, "lock", "--check").isSuccess
}
@Internal
suspend fun fixLock(sdk: Sdk, version: Version?): Result<String> {
if (version == null || version >= VERSION_2) {
return runPoetryWithSdk(sdk, "lock")
}
return runPoetryWithSdk(sdk, "lock", "--no-update")
}
@Internal
fun parsePoetryInstallDryRun(input: String): Pair<List<PyPackage>, List<PyRequirement>> {
val installedLines = listOf("Already installed", "Skipping", "Updating")