[python] PY-86999: Survive broken pyproject.toml.

`com.intellij.python.pyproject.PyProjectToml.Companion.parse` used to return Error in case of broken file so we simply ignored it. We now return `TomlTable` itself even in case of error.

So `com.intellij.python.pyproject.model.internal.pyProjectToml.TomFileToolsKt.readFile` is only null (which means module is skipped) if there is `IOException` reading `pyproject.toml` (either file doesn't exist or unreadable).

GitOrigin-RevId: 94fa7f46516450a9b0922679aa0d9d12571fae4f
This commit is contained in:
Ilya.Kazakevich
2026-01-20 23:36:52 +01:00
committed by intellij-monorepo-bot
parent 0ed3c5e373
commit 8c0deced79
9 changed files with 56 additions and 62 deletions

View File

@@ -56,6 +56,7 @@ jvm_library(
associates = [":pyproject"],
deps = [
"@lib//:kotlin-stdlib",
"//libraries/assertj-core",
"//python/openapi:community",
"//python/openapi:community_test_lib",
"//python/python-psi-impl:psi-impl",

View File

@@ -12,6 +12,7 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.libraries.assertj.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="module" module-name="intellij.python.psi.impl" />
<orderEntry type="module" module-name="intellij.platform.core" />

View File

@@ -4,12 +4,12 @@ package com.intellij.python.pyproject
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.Result
import com.jetbrains.python.Result.Companion.success
import com.jetbrains.python.sdk.findAmongRoots
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tuweni.toml.Toml
import org.apache.tuweni.toml.TomlParseError
import org.apache.tuweni.toml.TomlParseResult
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus.Internal
import java.nio.file.Path
@@ -78,7 +78,7 @@ data class PyProjectToml(
/**
* An instance of [TomlTable] provided by the TOML parser.
*/
val toml: TomlTable,
val toml: TomlParseResult,
) {
/**
* Gets a specific tool from an object implementing [PyProjectToolFactory].
@@ -103,9 +103,10 @@ data class PyProjectToml(
companion object {
/**
* TODO: REDOC
* Attempts to parse [inputStream] and construct an instance of [PyProjectToml].
* On success, returns an instance of [Result.Success] with an instance of [PyProjectToml].
* On failure, returns an instance of [Result.Failure] with a list of [TomlParseError]s.
* On failure, returns an instance of [Result.Failure] with a list of [TomlParseError]s and [TomlTable] itself.
*
* Example:
*
@@ -115,18 +116,15 @@ data class PyProjectToml(
* val hatch = pyProject.getTool(HatchPyProject)
* ```
*/
fun parse(tomlFileContent: String): Result<PyProjectToml, List<TomlParseError>> {
fun parse(tomlFileContent: String): PyProjectToml {
val issues = mutableListOf<PyProjectIssue>()
val toml = Toml.parse(tomlFileContent)
if (toml.hasErrors()) {
return Result.failure(toml.errors())
}
val projectTable = toml.safeGet<TomlTable>(PY_PROJECT_TOML_PROJECT).getOrIssue(issues)
if (projectTable == null) {
return success(PyProjectToml(null, issues, toml))
return PyProjectToml(null, issues, toml)
}
val name = projectTable.safeGet<String>("name").getOrIssue(issues) {
@@ -205,33 +203,31 @@ data class PyProjectToml(
val guiScripts = projectTable.parseMap("gui-scripts", issues)
val urls = projectTable.parseMap("urls", issues)
return success(
PyProjectToml(
PyProjectTable(
name,
version,
requiresPython,
authors,
maintainers,
description,
readme,
license,
licenseFiles,
keywords,
classifiers,
dynamic,
PyProjectDependencies(
projectDependencies,
devDependencies,
optionalDependencies
),
scripts,
guiScripts,
urls,
return PyProjectToml(
PyProjectTable(
name,
version,
requiresPython,
authors,
maintainers,
description,
readme,
license,
licenseFiles,
keywords,
classifiers,
dynamic,
PyProjectDependencies(
projectDependencies,
devDependencies,
optionalDependencies
),
issues,
toml,
)
scripts,
guiScripts,
urls,
),
issues,
toml,
)
}

View File

@@ -110,12 +110,13 @@ private suspend fun readFile(file: Path): PyProjectToml? {
logger.warn("Can't read $file", e)
return null
}
return when (val r = withContext(Dispatchers.Default) { PyProjectToml.parse(content) }) {
is Result.Failure -> {
logger.warn("Errors on $file: ${r.error.joinToString(", ")}")
null
return withContext(Dispatchers.Default) {
val toml = PyProjectToml.parse(content)
val errors = toml.issues.joinToString(", ")
if (errors.isNotBlank()) {
logger.warn("Errors on $file: $errors")
}
is Result.Success -> r.result
toml
}
}

View File

@@ -132,13 +132,14 @@ private suspend fun generatePyProjectTomlEntries(
participatedTools.add(toolAndName.first.id)
}
}
if (projectNameAsString != null) {
if (projectNameAsString in usedNamed) {
projectNameAsString = "$projectNameAsString@${usedNamed.size}"
}
usedNamed.add(projectNameAsString)
if (projectNameAsString == null) {
projectNameAsString = root.name
}
val projectName = ProjectName(projectNameAsString ?: "${root.name}@${tomlFile.hashCode()}")
if (projectNameAsString in usedNamed) {
projectNameAsString = "$projectNameAsString@${usedNamed.size}"
}
usedNamed.add(projectNameAsString)
val projectName = ProjectName(projectNameAsString)
val sourceRootsAndTools = Tool.EP.extensionList.flatMap { tool -> tool.getSrcRoots(toml.toml, root).map { Pair(tool, it) } }.toSet()
val sourceRoots = sourceRootsAndTools.map { it.second }.toSet() + findSrc(root)
participatedTools.addAll(sourceRootsAndTools.map { it.first.id })

View File

@@ -4,11 +4,9 @@ import com.intellij.python.pyproject.*
import com.intellij.python.pyproject.PyProjectIssue.*
import com.intellij.python.pyproject.TomlTableSafeGetError.RequiredValueMissing
import com.intellij.python.pyproject.TomlTableSafeGetError.UnexpectedType
import com.jetbrains.python.Result
import com.jetbrains.python.getOrThrow
import com.jetbrains.python.isFailure
import org.apache.tuweni.toml.TomlArray
import org.apache.tuweni.toml.TomlTable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
@@ -26,8 +24,7 @@ class PyProjectTomlTest {
val result = PyProjectToml.Companion.parse(configContents)
// THEN
assert(result.isFailure)
assert((result as Result.Failure).error.isNotEmpty())
Assertions.assertThat(result.toml.errors()).isNotEmpty()
}
@Test
@@ -45,7 +42,7 @@ class PyProjectTomlTest {
bar="test bar"
baz="test baz"
""".trimIndent()
val pyproject = PyProjectToml.Companion.parse(configContents).orThrow()
val pyproject = PyProjectToml.Companion.parse(configContents)
// WHEN
val testTool = pyproject.getTool(TestPyProject)
@@ -69,7 +66,7 @@ class PyProjectTomlTest {
""".trimIndent()
// WHEN
val pyproject = PyProjectToml.Companion.parse(configContents).orThrow()
val pyproject = PyProjectToml.Companion.parse(configContents)
val testTool = pyproject.getTool(TestPyProject)
// THEN
@@ -95,7 +92,7 @@ class PyProjectTomlTest {
""".trimIndent()
// WHEN
val pyproject = PyProjectToml.Companion.parse(configContents).orThrow()
val pyproject = PyProjectToml.Companion.parse(configContents)
val testTool = pyproject.getTool(TestPyProject)
// THEN
@@ -114,7 +111,7 @@ class PyProjectTomlTest {
name="Some project"
version="1.2.3"
""".trimIndent()
val pyproject = PyProjectToml.Companion.parse(configContents).orThrow()
val pyproject = PyProjectToml.Companion.parse(configContents)
// WHEN
val testTool = pyproject.getTool(TestPyProject)
@@ -128,7 +125,7 @@ class PyProjectTomlTest {
@MethodSource("parseTestCases")
fun parseTests(name: String, pyprojectToml: String, expectedProjectTable: PyProjectTable?, expectedIssues: List<PyProjectIssue>) {
val result = PyProjectToml.Companion.parse(pyprojectToml)
val unwrapped = result.getOrThrow()
val unwrapped = result
assertEquals(expectedProjectTable, unwrapped.project)
assertEquals(expectedIssues, unwrapped.issues)