mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[pycharm] PY-79369 Consolidate logic for pyproject.toml handling
GitOrigin-RevId: cf5b90daeaf3686b044e98888be6675618e9471e
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7b16a10379
commit
a570aebb18
@@ -173,5 +173,6 @@
|
||||
<orderEntry type="module" module-name="intellij.python.community.services.systemPython" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.services.internal.impl" />
|
||||
<orderEntry type="library" name="io.github.z4kn4fein.semver.jvm" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
||||
</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" />
|
||||
<orderEntry type="module-library">
|
||||
<library>
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tuweni/tuweni-toml/2.0.0/tuweni-toml-2.0.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.7.1/antlr4-runtime-4.7.1.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tuweni/tuweni-toml/2.0.0/tuweni-toml-2.0.0-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.7.1/antlr4-runtime-4.7.1-sources.jar!/" />
|
||||
</SOURCES>
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="module" module-name="intellij.python.psi.impl" />
|
||||
<orderEntry type="library" name="kotlinc.kotlin-compiler-tests" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.python.sdk" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util" />
|
||||
<orderEntry type="module" module-name="intellij.toml.core" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.intellij.python.pyproject
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.Result.Companion.failure
|
||||
import com.jetbrains.python.Result.Companion.success
|
||||
import com.jetbrains.python.mapResult
|
||||
import org.apache.tuweni.toml.TomlArray
|
||||
import org.apache.tuweni.toml.TomlTable
|
||||
import org.toml.lang.psi.TomlKeyValue as PsiTomlKeyValue
|
||||
import org.toml.lang.psi.TomlTable as PsiTomlTable
|
||||
import org.toml.lang.psi.TomlLiteral as PsiTomlLiteral
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class TomlTableSafeGetError {
|
||||
data class UnexpectedType(val path: String, val expected: KClass<*>, val actual: KClass<*>) : TomlTableSafeGetError()
|
||||
data class RequiredValueMissing(val path: String) : TomlTableSafeGetError()
|
||||
}
|
||||
|
||||
inline fun <reified T> TomlTable.safeGet(key: String): Result<T?, TomlTableSafeGetError.UnexpectedType> {
|
||||
val value = get("\"$key\"")
|
||||
|
||||
if (value == null) {
|
||||
return success(null)
|
||||
}
|
||||
|
||||
if (value !is T) {
|
||||
return failure(
|
||||
TomlTableSafeGetError.UnexpectedType(
|
||||
key,
|
||||
T::class,
|
||||
value::class,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return success(value)
|
||||
}
|
||||
|
||||
inline fun <reified T> TomlTable.safeGetRequired(key: String): Result<T, TomlTableSafeGetError> =
|
||||
safeGet<T>(key).mapResult {
|
||||
if (it == null) {
|
||||
failure(TomlTableSafeGetError.RequiredValueMissing(key))
|
||||
}
|
||||
else {
|
||||
success(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> TomlTable.safeGetArr(key: String): Result<List<T>?, TomlTableSafeGetError.UnexpectedType> {
|
||||
val array = safeGet<TomlArray>(key).getOr { return it }
|
||||
|
||||
if (array == null) {
|
||||
return success(null)
|
||||
}
|
||||
|
||||
return success(array.toList().mapIndexed { index, value ->
|
||||
if (value !is T) {
|
||||
return failure(TomlTableSafeGetError.UnexpectedType(
|
||||
"$key[$index]",
|
||||
T::class,
|
||||
value::class,
|
||||
))
|
||||
}
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
fun <T, E, I> Result<T, E>.getOrIssue(issues: MutableList<I>, wrapper: (E) -> I, onNull: (() -> Unit)? = null): T? =
|
||||
when (this) {
|
||||
is Result.Success -> {
|
||||
if (result == null && onNull != null) {
|
||||
onNull()
|
||||
}
|
||||
result
|
||||
}
|
||||
is Result.Failure -> {
|
||||
issues += wrapper(error)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun PsiElement.findTomlHeader(name: String): PsiElement? =
|
||||
children.find { element ->
|
||||
(element as? PsiTomlTable)?.header?.key?.text == name
|
||||
}
|
||||
|
||||
fun PsiElement.findTomlValueByKey(name: String): PsiElement? =
|
||||
(children.find { element ->
|
||||
(element as? PsiTomlKeyValue)?.key?.text == name
|
||||
} as? PsiTomlKeyValue)?.value
|
||||
|
||||
fun PsiElement.findTomlLiteralsContaining(text: String): List<PsiElement> =
|
||||
children.filter { element ->
|
||||
(element as? PsiTomlLiteral)?.text?.contains(text) ?: false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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.pyproject
|
||||
|
||||
data class PyProjectTable(
|
||||
val name: String? = null,
|
||||
val version: String? = null,
|
||||
val requiresPython: String? = null,
|
||||
val authors: List<PyProjectContact>? = null,
|
||||
val maintainers: List<PyProjectContact>? = null,
|
||||
val description: String? = null,
|
||||
val readme: PyProjectFile? = null,
|
||||
val license: String? = null,
|
||||
val licenseFiles: List<String>? = null,
|
||||
val keywords: List<String>? = null,
|
||||
val classifiers: List<String>? = null,
|
||||
val dynamic: List<String>? = null,
|
||||
val dependencies: PyProjectDependencies = PyProjectDependencies(),
|
||||
val scripts: Map<String, String>? = null,
|
||||
val guiScripts: Map<String, String>? = null,
|
||||
val urls: Map<String, String>? = null,
|
||||
)
|
||||
|
||||
data class PyProjectDependencies(
|
||||
val project: List<String> = listOf(),
|
||||
val dev: List<String> = listOf(),
|
||||
val optional: Map<String, List<String>> = mapOf(),
|
||||
)
|
||||
|
||||
data class PyProjectFile(
|
||||
val name: String,
|
||||
val contentType: String? = null,
|
||||
)
|
||||
|
||||
data class PyProjectContact(val name: String?, val email: String?) {
|
||||
init {
|
||||
if (name == null && email == null) {
|
||||
throw IllegalArgumentException("at least name or email should be provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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.pyproject
|
||||
|
||||
import com.jetbrains.python.Result.Companion.failure
|
||||
import com.jetbrains.python.Result.Companion.success
|
||||
import com.jetbrains.python.Result
|
||||
import org.apache.tuweni.toml.Toml
|
||||
import org.apache.tuweni.toml.TomlParseError
|
||||
import org.apache.tuweni.toml.TomlTable
|
||||
import java.io.InputStream
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.findAmongRoots
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
const val PY_PROJECT_TOML: String = "pyproject.toml"
|
||||
|
||||
sealed class PyProjectIssue {
|
||||
data object MissingName : PyProjectIssue()
|
||||
data object MissingVersion : PyProjectIssue()
|
||||
data class SafeGetError(val error: TomlTableSafeGetError) : PyProjectIssue()
|
||||
data class InvalidContact(val path: String) : PyProjectIssue()
|
||||
}
|
||||
|
||||
data class PyProjectToml(
|
||||
val project: PyProjectTable?,
|
||||
val issues: List<PyProjectIssue>,
|
||||
val toml: TomlTable,
|
||||
) {
|
||||
fun <T : PyProjectToolFactory<U>, U> getTool(tool: T): U {
|
||||
return tool.createTool(
|
||||
mapOf(
|
||||
*tool.tables.map {
|
||||
it to toml.getTable(it)
|
||||
}.toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(inputStream: InputStream): Result<PyProjectToml, List<TomlParseError>> {
|
||||
val issues = mutableListOf<PyProjectIssue>()
|
||||
val toml = Toml.parse(inputStream)
|
||||
|
||||
if (toml.hasErrors()) {
|
||||
return failure(toml.errors())
|
||||
}
|
||||
|
||||
val projectTable = toml.safeGet<TomlTable>("project").getOrIssue(issues)
|
||||
|
||||
if (projectTable == null) {
|
||||
return success(PyProjectToml(null, issues, toml))
|
||||
}
|
||||
|
||||
val name = projectTable.safeGet<String>("name").getOrIssue(issues) {
|
||||
issues += PyProjectIssue.MissingName
|
||||
}
|
||||
|
||||
val dynamic = projectTable.safeGetArr<String>("dynamic").getOrIssue(issues)
|
||||
val version = projectTable.safeGet<String>("version").getOrIssue(issues) {
|
||||
if (dynamic?.contains("version") != true) {
|
||||
issues += PyProjectIssue.MissingVersion
|
||||
}
|
||||
}
|
||||
|
||||
val requiresPython = projectTable.safeGet<String>("requires-python").getOrIssue(issues)
|
||||
val authors = projectTable.parseContacts("authors", issues)
|
||||
val maintainers = projectTable.parseContacts("maintainers", issues)
|
||||
val description = projectTable.safeGet<String>("description").getOrIssue(issues)
|
||||
val license = projectTable.safeGet<String>("license").getOrIssue(issues)
|
||||
val licenseFiles = projectTable.safeGetArr<String>("license-files").getOrIssue(issues)
|
||||
val keywords = projectTable.safeGetArr<String>("keywords").getOrIssue(issues)
|
||||
val classifiers = projectTable.safeGetArr<String>("classifiers").getOrIssue(issues)
|
||||
|
||||
val readme = when (val res = projectTable.safeGet<String>("readme")) {
|
||||
is Result.Success -> {
|
||||
res.getOrIssue(issues)?.let { name ->
|
||||
PyProjectFile(name)
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
val table = projectTable
|
||||
.safeGet<TomlTable>("readme")
|
||||
.getOrIssue(issues)
|
||||
|
||||
val name = table
|
||||
?.safeGetRequired<String>("name")
|
||||
?.getOrIssue(issues)
|
||||
|
||||
val contentType = table
|
||||
?.safeGetRequired<String>("content-type")
|
||||
?.getOrIssue(issues)
|
||||
|
||||
if (name != null && contentType != null) {
|
||||
PyProjectFile(name, contentType)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val projectDependencies = projectTable.safeGetArr<String>("dependencies").getOrIssue(issues) ?: listOf()
|
||||
val devDependencies =
|
||||
toml
|
||||
.safeGet<TomlTable>("dependency-groups")
|
||||
.getOrIssue(issues)
|
||||
?.safeGetArr<String>("dev")
|
||||
?.getOrIssue(issues)
|
||||
?.toList()
|
||||
?: listOf()
|
||||
val optionalDependencies =
|
||||
projectTable
|
||||
.safeGet<TomlTable>("optional-dependencies")
|
||||
.getOrIssue(issues)
|
||||
?.let { table ->
|
||||
mapOf(
|
||||
*table.keySet().mapNotNull { key ->
|
||||
table.safeGetArr<String>(key).getOrIssue(issues)?.let { value ->
|
||||
key to value
|
||||
}
|
||||
}.toTypedArray()
|
||||
)
|
||||
}
|
||||
?: mapOf()
|
||||
|
||||
val scripts = projectTable.parseMap("scripts", issues)
|
||||
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,
|
||||
),
|
||||
issues,
|
||||
toml,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun findFileBlocking(module: Module): VirtualFile? =
|
||||
findAmongRoots(module, PY_PROJECT_TOML)
|
||||
|
||||
suspend fun findFile(module: Module): VirtualFile? =
|
||||
withContext(Dispatchers.IO) {
|
||||
findAmongRoots(module, PY_PROJECT_TOML)
|
||||
}
|
||||
|
||||
suspend fun findModuleWorkingDirectory(module: Module): Pair<VirtualFile?, Path?> {
|
||||
val file = findFile(module)
|
||||
return Pair(file, file?.toNioPathOrNull()?.parent ?: module.basePath?.let { Path.of(it) })
|
||||
}
|
||||
|
||||
fun findProjectWorkingDirectory(project: Project): Path? =
|
||||
project.basePath?.let { Path.of(it) }
|
||||
|
||||
|
||||
private fun TomlTable.parseContacts(
|
||||
key: String,
|
||||
issues: MutableList<PyProjectIssue>,
|
||||
): List<PyProjectContact>? {
|
||||
val table = safeGetArr<TomlTable>(key).getOrIssue(issues) ?: return null
|
||||
return table.mapIndexedNotNull { index, authorTable ->
|
||||
val name = authorTable.safeGet<String>("name").getOrIssue(issues)
|
||||
val email = authorTable.safeGet<String>("email").getOrIssue(issues)
|
||||
|
||||
if (name == null && email == null) {
|
||||
issues += PyProjectIssue.InvalidContact("$key[$index]")
|
||||
return@mapIndexedNotNull null
|
||||
}
|
||||
|
||||
PyProjectContact(name, email)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TomlTable.parseMap(key: String, issues: MutableList<PyProjectIssue>): Map<String, String>? {
|
||||
val table = safeGet<TomlTable>(key).getOrIssue(issues) ?: return null
|
||||
return mapOf(
|
||||
*table.keySet().mapNotNull { key ->
|
||||
table.safeGet<String>(key).getOrIssue(issues)?.let { value ->
|
||||
key to value
|
||||
}
|
||||
}.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> Result<T, TomlTableSafeGetError>.getOrIssue(issues: MutableList<PyProjectIssue>, onNull: (() -> Unit)? = null) =
|
||||
getOrIssue(issues, { PyProjectIssue.SafeGetError(it) }, onNull)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
package com.intellij.python.pyproject
|
||||
|
||||
import org.apache.tuweni.toml.TomlTable
|
||||
|
||||
interface PyProjectToolFactory<T> {
|
||||
val tables: List<String>
|
||||
fun createTool(tables: Map<String, TomlTable?>): T
|
||||
}
|
||||
Reference in New Issue
Block a user