PY-86614 additional FUS logging for python tools

+ more tools added (including pixi)
+ scan for file markers (like pixi.toml, uv.lock, poetry.lock, etc)

Merge-request: IJ-MR-186939
Merged-by: Vitaly Legchilkin <Vitaly.Legchilkin@jetbrains.com>

GitOrigin-RevId: e9dfe9e4d1f5719c9ab387ec0159735e4d893eae
This commit is contained in:
Vitaly Legchilkin
2026-01-05 21:03:13 +00:00
committed by intellij-monorepo-bot
parent c0eb091568
commit f37cdcfc8d
33 changed files with 543 additions and 190 deletions
@@ -3,13 +3,8 @@ package com.intellij.python.junit5Tests.framework.metaInfo
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.TestDataPath
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.Extension
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.*
import org.junit.jupiter.api.extension.ExtensionContext.Namespace
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import java.nio.file.Path
import kotlin.io.path.relativeTo
import kotlin.jvm.optionals.getOrNull
@@ -67,11 +62,14 @@ internal class TestMetaInfoExtension : BeforeAllCallback, BeforeEachCallback, Ex
* Calculates a real test data path based on class annotations (resolves $CONTENT_ROOT placeholder).
*/
override fun beforeAll(context: ExtensionContext) {
val testClassInfo = getAnnotation(context, TestClassInfo::class.java)
?: error("Add ${TestClassInfo::class} class level")
val testDataPathWithPlaceholders = getAnnotation(context, TestDataPath::class.java)?.value
val testDataPath = testDataPathWithPlaceholders?.let { testClassInfo.resolvePath(it) }
val testDataPath = testDataPathWithPlaceholders?.let { pathWithPlaceholders ->
val testClassInfo = getAnnotation(context, TestClassInfo::class.java)
?: error("Add ${TestClassInfo::class} class level")
testClassInfo.resolvePath(pathWithPlaceholders)
}
val data = TestClassInfoData(
testDataPath = testDataPath,
@@ -543,7 +543,6 @@
<statistics.projectUsagesCollector implementation="com.jetbrains.python.statistics.PyInterpreterUsagesCollector"/>
<statistics.projectUsagesCollector implementation="com.jetbrains.python.statistics.PyPackageVersionUsagesCollector"/>
<statistics.projectUsagesCollector implementation="com.jetbrains.python.statistics.PyPackageInEditorUsageCollector"/>
<statistics.projectUsagesCollector implementation="com.jetbrains.python.statistics.PyProjectTomlUsageCollector"/>
<statistics.projectUsagesCollector implementation="com.jetbrains.python.statistics.PyCompatibilityInspectionCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.namespacePackages.PyNamespacePackagesStatisticsCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.codeInsight.codeVision.PyCodeVisionUsageCollector"/>
+5
View File
@@ -42,6 +42,8 @@ jvm_library(
"//platform/lang-impl",
"//platform/lang-core",
"//platform/backend/observation",
"//platform/statistics",
"//platform/indexing-api:indexing",
],
exports = ["@lib//:tuweni-toml"]
)
@@ -89,6 +91,9 @@ jvm_library(
"//platform/testFramework:testFramework_test_lib",
"//python/python-venv:community-impl-venv",
"//python/python-venv:community-impl-venv_test_lib",
"//platform/statistics",
"//platform/statistics:statistics_test_lib",
"//platform/indexing-api:indexing",
],
exports = ["@lib//:tuweni-toml"]
)
@@ -40,5 +40,7 @@
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.indexing" />
</component>
</module>
@@ -11,6 +11,8 @@
<extensions defaultExtensionNs="com.intellij">
<registryKey defaultValue="false" description="Load project structure from pyproject.toml" key="intellij.python.pyproject.model" restartRequired="true"/>
<postStartupActivity implementation="com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity"/>
<statistics.projectUsagesCollector implementation="com.intellij.python.pyproject.statistics.PythonTomlStatsUsagesCollector"/>
</extensions>
<extensionPoints>
<extensionPoint qualifiedName="com.intellij.python.pyproject.model.tool" interface="com.intellij.python.pyproject.model.spi.Tool"
@@ -28,7 +28,10 @@ const val PY_PROJECT_TOML_PROJECT: String = "project"
const val PY_PROJECT_TOML_BUILD_SYSTEM: String = "build-system"
@Internal
const val PY_PROJECT_TOML_TOOL_PREFIX: String = "tool."
const val PY_PROJECT_TOML_DEPENDENCY_GROUPS: String = "dependency-groups"
@Internal
const val PY_PROJECT_TOML_TOOL_PREFIX: String = "tool"
/**
* Represents an issue that could occur in [PyProjectToml.parse].
@@ -0,0 +1,237 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyproject.statistics
import com.intellij.internal.statistic.beans.MetricEvent
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.FUS_RECORDER
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.ProjectUsagesCollector
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.findPsiFile
import com.intellij.psi.PsiFile
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.ProjectScope
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.pyproject.PY_PROJECT_TOML_BUILD_SYSTEM
import com.intellij.python.pyproject.PY_PROJECT_TOML_DEPENDENCY_GROUPS
import com.intellij.python.pyproject.PY_PROJECT_TOML_TOOL_PREFIX
import com.jetbrains.python.packaging.PyPackageName
import com.jetbrains.python.packaging.PyRequirementParser
import org.toml.lang.psi.TomlArray
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlLiteral
import org.toml.lang.psi.TomlTable
import java.util.concurrent.atomic.AtomicInteger
internal enum class PythonTool(val normalizedName: String, val markerFileNames: Set<String>) {
AUTOFLAKE("autoflake"),
AUTOIMPORT("autoimport"),
BASEDPYRIGHT("basedpyright"),
BANDIT("bandit", ".bandit"),
BLACK("black", "black.toml"),
CIBUILDWHEEL("cibuildwheel"),
CHECK_JSONSCHEMA("check-jsonschema"),
CODESPELL("codespell", ".codespellrc"),
COMFY("comfy"),
CONDA_LOCK("conda-lock"),
COVERAGE("coverage", ".coveragerc"),
CYTHON("cython"),
DAGSTER("dagster"),
DARKER("darker"),
DEPTRY("deptry"),
DOCFORMATTER("docformatter"),
FLIT("flit", "flit.ini"),
FLIT_CORE("flit-core"),
FLAKE8("flake8", ".flake8"),
GREAT_EXPECTATIONS("great-expectations", "great_expectations.yml"),
HATCH("hatch", "hatch.toml", "hatch.lock"),
HATCHLING("hatchling"),
HATCH_VCS("hatch-vcs"),
HYPOTHESIS("hypothesis", ".hypothesis"),
ISORT("isort", ".isort.cfg"),
MAKE_ENV("make-env"),
MKDOCSTRINGS("mkdocstrings"),
MYST_PARSER("myst-parser"),
MYPY("mypy", "mypy.ini"),
NBQA("nbqa"),
NINJA("ninja"),
NOX("nox", "noxfile.py"),
PDOC("pdoc"),
PDM("pdm", "pdm.lock"),
PIXI("pixi", "pixi.toml", "pixi.lock"),
POETRY("poetry", "poetry.lock"),
POETRY_CORE("poetry-core"),
POE("poe"),
PREFECT("prefect", "prefect.yaml"),
PYBIND11("pybind11"),
PYCLN("pycln"),
PYDANTIC_MYPY("pydantic-mypy"),
PYRIGHT("pyright", "pyrightconfig.json"),
PY_SPY("py-spy"),
PYTKDOCS("pytkdocs"),
PYTONIQ("pytoniq"),
PYUPGRADE("pyupgrade"),
REFURB("refurb"),
RUFF("ruff", "ruff.toml"),
SAFETY("safety"),
SCITK_BUILD("scikit-build"),
SCITK_BUILD_CORE("scikit-build-core"),
SEMATIC_RELEASE("sematic-release"),
SETUPTOOLS("setuptools", "setup.py", "setup.cfg"),
SETUPTOOLS_RUST("setuptools-rust"),
SETUPTOOLS_SCM("setuptools-scm"),
SPHINX("sphinx"),
TOX("tox", "tox.ini", "tox.toml"),
UV("uv", "uv.lock"),
VALIDATE_PYPROJECT("validate-pyproject"),
VULTURE("vulture"),
WHEEL("wheel"),
YAPF("yapf", ".style.yapf");
/**
* For backward compatibility using normalized name instead of Enum name for FUS.
*/
val fusName: String get() = normalizedName
constructor(key: String, vararg markerFileNames: String) : this(key, markerFileNames.toSet())
companion object {
fun findByNormalizedName(normalizedName: String): PythonTool? = entries.find { it.normalizedName == normalizedName }
}
}
internal val TRACKED_DEPENDENCY_GROUPS = listOf(
"all", "async", "bench", "build", "ci",
"cli", "coverage", "db", "debug", "deploy",
"dev", "docs", "examples", "extras", "extras-all",
"format", "gpu", "lint", "optional", "profile",
"security", "test", "tooling", "typing", "viz"
)
internal const val DEPENDENCY_GROUP_OTHER = "other"
private val GROUP = EventLogGroup("python.toml.stats", 2, FUS_RECORDER, "Python Project Statistics")
internal val PYTHON_PYPROJECT_TOOLS = GROUP.registerEvent(
"python.pyproject.tools",
EventFields.Enum("name", PythonTool::class.java) { it.fusName },
"A Python tool defined in the [tool.*] table of pyproject.toml"
)
// https://peps.python.org/pep-0518/
internal val PYTHON_PYPROJECT_BUILDSYSTEM = GROUP.registerEvent(
"python.pyproject.buildsystem",
EventFields.Enum("name", PythonTool::class.java) { it.fusName },
"A Python tool defined in build-system.requires of pyproject.toml"
)
internal val PYTHON_TOOL_MARKERS_DETECTED = GROUP.registerEvent(
"python.tool.markers.detected",
EventFields.Enum("name", PythonTool::class.java) { it.fusName },
"A Python tool detected via tool marker files (e.g., uv.lock, hatch.toml)"
)
internal val PYTHON_PYPROJECT_DEPENDENCY_GROUP = GROUP.registerEvent(
"python.pyproject.dependency.group",
EventFields.String("name", TRACKED_DEPENDENCY_GROUPS + DEPENDENCY_GROUP_OTHER),
"A dependency group from pyproject.toml"
)
internal val PYTHON_PYPROJECT_COUNT = GROUP.registerEvent(
"python.pyproject.count",
EventFields.Int("count"),
"Number of pyproject.toml files (python projects) in the workspace"
)
internal class PythonTomlStatsUsagesCollector : ProjectUsagesCollector() {
override fun getGroup(): EventLogGroup = GROUP
override fun requiresReadAccess() = true
override fun requiresSmartMode() = true
override fun getMetrics(project: Project): Set<MetricEvent> {
val scope = ProjectScope.getContentScope(project)
val pyProjectTomlCounter = AtomicInteger(0)
val tools = mutableSetOf<PythonTool>()
val buildSystems = mutableSetOf<PythonTool>()
val dependencyGroups = mutableSetOf<String>()
FilenameIndex.processFilesByName(PY_PROJECT_TOML, true, scope) { virtualFile ->
virtualFile.findPsiFile(project)?.takeIf { it.isValid }?.let { psiFile ->
pyProjectTomlCounter.incrementAndGet()
tools.addAll(PyProjectTomlCollector.findDeclaredTools(psiFile))
buildSystems.addAll(PyProjectTomlCollector.findBuildSystemRequiresTools(psiFile))
dependencyGroups.addAll(PyProjectTomlCollector.findDependencyGroups(psiFile))
}
true
}
val toolsDetectedByMarkers = PythonTool.entries.filterNot { tool ->
FilenameIndex.processFilesByNames(tool.markerFileNames, true, scope, null) { false }
}
val metrics = mutableSetOf<MetricEvent>()
metrics.add(PYTHON_PYPROJECT_COUNT.metric(pyProjectTomlCounter.get()))
tools.mapTo(metrics) { PYTHON_PYPROJECT_TOOLS.metric(it) }
buildSystems.mapTo(metrics) { PYTHON_PYPROJECT_BUILDSYSTEM.metric(it) }
dependencyGroups.mapTo(metrics) { PYTHON_PYPROJECT_DEPENDENCY_GROUP.metric(it) }
toolsDetectedByMarkers.mapTo(metrics) { PYTHON_TOOL_MARKERS_DETECTED.metric(it) }
return metrics
}
}
internal object PyProjectTomlCollector {
fun findDeclaredTools(file: PsiFile): Set<PythonTool> {
val declaredTools = file.children.mapNotNullTo(mutableSetOf()) { element ->
val toolTomlKey = (element as? TomlTable)?.header?.key?.takeIf {
it.segments.firstOrNull()?.text == PY_PROJECT_TOML_TOOL_PREFIX
} ?: return@mapNotNullTo null
val toolNormalizedName = toolTomlKey.segments.getOrNull(1)?.text?.let {
PyPackageName.normalizePackageName(it)
}
toolNormalizedName?.let { PythonTool.findByNormalizedName(it) }
}
return declaredTools
}
fun findBuildSystemRequiresTools(file: PsiFile): Set<PythonTool> {
val buildSystemTables = file.children.mapNotNull { psiElement ->
(psiElement as? TomlTable)?.takeIf { it.header.key?.text == PY_PROJECT_TOML_BUILD_SYSTEM }
}
val requiresValues = buildSystemTables.flatMap { tomlTable ->
tomlTable.children.mapNotNull { line ->
(line as? TomlKeyValue)?.takeIf { kv -> kv.key.text == "requires" }?.value
}
}
val literals = requiresValues.flatMap { tomlValue ->
(tomlValue as? TomlArray)?.elements?.mapNotNull { (it as? TomlLiteral)?.text } ?: emptyList()
}
val buildTools = literals.mapNotNullTo(mutableSetOf()) {
val requirement = PyRequirementParser.fromLine(it.removeSurrounding("\""))
requirement?.name?.let { key -> PythonTool.findByNormalizedName(key) }
}
return buildTools
}
fun findDependencyGroups(file: PsiFile): List<String> {
val dependencyGroupTables = file.children.mapNotNull { psiElement ->
(psiElement as? TomlTable)?.takeIf { it.header.key?.text == PY_PROJECT_TOML_DEPENDENCY_GROUPS }
}
val dependencyGroups = dependencyGroupTables.flatMap { tomlTable ->
tomlTable.children.mapNotNull { (it as? TomlKeyValue)?.key?.text }
}.distinct().map { it.takeIf { it in TRACKED_DEPENDENCY_GROUPS } ?: DEPENDENCY_GROUP_OTHER }.sorted()
return dependencyGroups
}
}
@@ -1,24 +1,53 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging
package com.intellij.python.junit5Tests.unit.pyproject
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFileFactory
import com.intellij.python.junit5Tests.framework.PyDefaultTestApplication
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.jetbrains.python.fixtures.PyTestCase
import com.jetbrains.python.statistics.PyProjectTomlUsageCollector
import com.intellij.python.pyproject.statistics.DEPENDENCY_GROUP_OTHER
import com.intellij.python.pyproject.statistics.PyProjectTomlCollector
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.common.timeoutRunBlocking
import kotlinx.coroutines.Dispatchers
import org.junit.jupiter.api.Test
import org.toml.lang.psi.TomlFileType
class PyProjectTomlStatsTest : PyTestCase() {
fun doTest(text: String, toolNames: Set<String>, backendNames: Set<String>) {
val psiFile = myFixture.configureByText(PY_PROJECT_TOML, text)
@PyDefaultTestApplication
class PyProjectTomlCollectorTest(val project: Project) {
fun doTest(
text: String,
toolNames: Set<String> = emptySet(),
backendNames: Set<String> = emptySet(),
dependencyGroupsNames: Set<String> = emptySet(),
) = timeoutRunBlocking(context = Dispatchers.EDT) {
val psiFile = PsiFileFactory.getInstance(project).createFileFromText(PY_PROJECT_TOML, TomlFileType, text)
val tools = mutableSetOf<String>()
val backends = mutableSetOf<String>()
val tools = PyProjectTomlCollector.findDeclaredTools(psiFile).map { it.fusName }
val backends = PyProjectTomlCollector.findBuildSystemRequiresTools(psiFile).map { it.fusName }
val dependencyGroups = PyProjectTomlCollector.findDependencyGroups(psiFile)
PyProjectTomlUsageCollector.collectTools(psiFile, tools)
PyProjectTomlUsageCollector.collectBuildBackends(psiFile, backends)
assertSameElements(tools, toolNames)
assertSameElements(backends, backendNames)
UsefulTestCase.assertSameElements(tools, toolNames)
UsefulTestCase.assertSameElements(backends, backendNames)
UsefulTestCase.assertSameElements(dependencyGroups, dependencyGroupsNames)
}
@Test
fun testDependencyGroups() {
val text = """
[dependency-groups]
test = ["pytest<8", "coverage"]
typing = ["mypy==1.7.1", "types-requests"]
[dependency-groups]
lint = ["black", "flake8"]
typing-test = [{include-group = "typing"}, "pytest<8"]
""".trimIndent()
doTest(text, dependencyGroupsNames = setOf("test", "typing", "lint", DEPENDENCY_GROUP_OTHER))
}
@Test
fun testEmptyFile() {
val text = """
""".trimIndent()
@@ -26,6 +55,7 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, emptySet(), emptySet())
}
@Test
fun testInvalidFile() {
val text = """
some abradackadabra
@@ -36,6 +66,7 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, emptySet(), emptySet())
}
@Test
fun testEmptyToml() {
val text = """
[build-system]
@@ -46,6 +77,7 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, emptySet(), emptySet())
}
@Test
fun testDeduplicateTools() {
val text = """
[tool.ruff.isort.sections]
@@ -61,18 +93,20 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, setOf("ruff"), emptySet())
}
@Test
fun testNormalizeTools() {
val text = """
[tool.ruff_furr.isort.sections]
[tool.py_spy.isort.sections]
"nextgisweb_env_lib" = ["nextgisweb.env", "nextgisweb.lib"]
"nextgisweb_comp" = ["nextgisweb"]
""".trimIndent()
doTest(text, setOf("ruff-furr"), emptySet())
doTest(text, setOf("py-spy"), emptySet())
}
@Test
fun testCollectBuilds() {
val text = """
[build-system]
@@ -83,6 +117,7 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, emptySet(), setOf("hatchling", "setuptools", "flit"))
}
@Test
fun testDeduplicateBuilds() {
val text = """
[build-system]
@@ -93,6 +128,7 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, emptySet(), setOf("hatchling", "flit"))
}
@Test
fun testCommentedBuilds() {
val text = """
[build-system]
@@ -106,16 +142,18 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, emptySet(), setOf("hatchling"))
}
@Test
fun testNormalizeBuilds() {
val text = """
[build-system]
requires = ["flit_core", "setup.tools >= 61", "flit "]
requires = ["flit_core", "setuptools >= 61", "flit "]
build-backend = "xxxxyyy.build"
""".trimIndent()
doTest(text, emptySet(), setOf("flit", "setup-tools", "flit-core"))
doTest(text, emptySet(), setOf("flit", "setuptools", "flit-core"))
}
@Test
fun testPyPiSimpleExample() {
val text = """
[build-system]
@@ -158,6 +196,7 @@ class PyProjectTomlStatsTest : PyTestCase() {
doTest(text, setOf("hatch", "mypy", "pydantic-mypy"), setOf("hatchling"))
}
@Test
fun testPyProjectExample() {
val text = """
[build-system]
@@ -0,0 +1,73 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.junit5Tests.unit.pyproject
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.python.junit5Tests.framework.PyDefaultTestApplication
import com.intellij.python.junit5Tests.framework.metaInfo.Repository
import com.intellij.python.junit5Tests.framework.metaInfo.TestClassInfo
import com.intellij.python.pyproject.statistics.*
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.common.timeoutRunBlocking
import kotlinx.coroutines.Dispatchers
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@PyDefaultTestApplication
@TestClassInfo(Repository.PY_COMMUNITY)
@TestDataPath($$"$CONTENT_ROOT/../python-pyproject/testData/statistics/tools")
class PythonTomlStatsUsagesCollectorTest(val project: Project) {
@Test
fun testMonorepo() = timeoutRunBlocking(context = Dispatchers.EDT) {
val metrics = PythonTomlStatsUsagesCollector().collect(project)
val groups = metrics.groupBy({ it.eventId }, { it.data.build() })
assertEquals(
2,
groups[PYTHON_PYPROJECT_COUNT.eventId]!!.single().values.first() as Int
) {
"Number of pyproject.toml files does not match the test data"
}
// "viz" was selected as a non-declared group randomly to cover a not-found case
val dependencyGroups = groups[PYTHON_PYPROJECT_DEPENDENCY_GROUP.eventId]!!.map { it.values.first() as String }
assertEquals(
(TRACKED_DEPENDENCY_GROUPS + DEPENDENCY_GROUP_OTHER - listOf("viz")).sorted(),
dependencyGroups.sorted(),
) {
"Dependency groups logged do not match the test data"
}
// ".bandit" was selected as a non-declared tool randomly to cover a not-found case
val detectedToolsViaMarkers = groups[PYTHON_TOOL_MARKERS_DETECTED.eventId]!!.map { it.values.first() as String }
assertEquals(
(PythonTool.entries.filter { it.markerFileNames.isNotEmpty() && it != PythonTool.BANDIT }).map { it.fusName }.sorted(),
detectedToolsViaMarkers.sorted(),
) {
"Detected tools via marker files do not match the test data"
}
// tools randomly distributed across [build-system.requires] and [tool.*]
val buildSystemTools = setOf(
PythonTool.SETUPTOOLS, PythonTool.WHEEL, PythonTool.SETUPTOOLS_SCM, PythonTool.SETUPTOOLS_RUST,
PythonTool.FLIT_CORE, PythonTool.HATCHLING, PythonTool.POETRY_CORE, PythonTool.SCITK_BUILD_CORE,
PythonTool.CYTHON, PythonTool.PYBIND11, PythonTool.NINJA, PythonTool.SEMATIC_RELEASE, PythonTool.SCITK_BUILD
)
val toolsFromBuildSystem = groups[PYTHON_PYPROJECT_BUILDSYSTEM.eventId]!!.map { it.values.first() as String }
assertEquals(
(PythonTool.entries.filter { it in buildSystemTools }).map { it.fusName }.sorted(),
toolsFromBuildSystem.sorted(),
) {
"Detected tools via [build-system.requires] do not match the test data"
}
val toolsFromPyProjectToml = groups[PYTHON_PYPROJECT_TOOLS.eventId]!!.map { it.values.first() as String }
assertEquals(
(PythonTool.entries.filter { it !in buildSystemTools }).map { it.fusName }.sorted(),
toolsFromPyProjectToml.sorted(),
) {
"Detected tools [tool.*] via pyproject.toml do not match the test data"
}
}
}
@@ -0,0 +1,119 @@
[build-system]
requires = [
"setuptools>=68",
"wheel>=0.41",
"setuptools_scm>=8",
"setuptools_rust>=1.8",
"flit-core>=3.9",
"hatchling>=1.21",
"poetry-core>=1.9",
"scikit-build-core>=0.9",
"cython>=3.0",
"pybind11>=2.11",
]
build-backend = "setuptools.build_meta"
[tool]
[tool.bandit]
[tool.black]
[tool.codespell]
[tool.coverage]
[tool.darker]
[tool.deptry]
[tool.docformatter]
[tool.flake8]
[tool.hatch.xxx]
[tool.hatch-vcs]
[tool.hypothesis]
[tool.isort]
[tool.mkdocstrings]
[tool.myst-parser]
[tool.mypy]
[tool.nbqa]
[tool.nox]
[tool.pdm]
[tool.poetry]
[tool.prefect]
[tool.pycln]
[tool.pydantic-mypy]
[tool.pyright]
[tool.pyupgrade]
[tool.refurb]
[tool.ruff]
[tool.safety]
[tool.sphinx]
[tool.tox]
[tool.uv]
[tool.validate-pyproject]
[tool.vulture]
[tool.yapf]
[dependency-groups]
all = ["asyncio", "rich", "httpx", "pytest", "ruff", "mypy"]
async = ["anyio", "async-timeout", "aiofiles", "httpx"]
bench = ["pytest-benchmark", "perfplot", "timeit-decorator"]
build = ["build", "twine", "wheel"]
ci = ["pytest", "coverage[toml]", "ruff", "mypy", "tox"]
cli = ["click", "rich", "typer"]
coverage = ["coverage[toml]", "pytest-cov"]
db = ["sqlalchemy", "alembic", "psycopg2-binary"]
debug = ["ipdb", "debugpy", "rich"]
deploy = ["gunicorn", "uvicorn"]
dev = ["ipython", "pre-commit", "watchfiles"]
docs = ["sphinx", "mkdocs", "mkdocstrings[python]", "myst-parser"]
examples = ["jupyter", "notebook", "matplotlib"]
extras = ["orjson", "ujson"]
extras-all = ["orjson", "ujson", "msgpack"]
format = ["black", "isort", "docformatter"]
gpu = ["cupy", "torch"]
lint = ["ruff", "flake8", "pylint"]
optional = ["importlib-metadata"]
profile = ["line-profiler", "memory-profiler"]
security = ["bandit", "safety"]
test = ["pytest", "hypothesis", "pytest-xdist"]
tooling = ["nox", "pre-commit", "tox"]
typing = ["mypy", "types-requests", "types-setuptools"]
# viz = ["matplotlib", "seaborn", "plotly"]
# ─────────────────────────
# Additional groups, not listed in allowed (will be logged as "other")
# ─────────────────────────
logging = ["structlog", "loguru"]
network = ["requests", "urllib3", "websockets"]
performance = ["orjson", "ujson", "cython"]
serialization = ["pydantic", "msgpack", "pyyaml"]
@@ -0,0 +1,37 @@
[build-system]
requires = [
"scikit-build",
"sematic-release",
"ninja>=1.11",
]
build-backend = "setuptools.build_meta"
[tool.autoflake]
[tool.autoimport]
[tool.basedpyright]
[tool.check-jsonschema]
[tool.cibuildwheel]
[tool.comfy]
[tool.conda-lock]
[tool.dagster]
[tool.flit]
[tool.great-expectations]
[tool.make-env]
[tool.mkdoc]
[tool.pdoc]
[tool.pixi]
[tool.poe]
[tool.py-spy]
[tool.pytkdocs]
[tool.pytoniq]
@@ -1,161 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.statistics
import com.intellij.internal.statistic.beans.MetricEvent
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.ProjectUsagesCollector
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.findPsiFile
import com.intellij.psi.PsiFile
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.ProjectScope
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.pyproject.PY_PROJECT_TOML_BUILD_SYSTEM
import com.intellij.python.pyproject.PY_PROJECT_TOML_TOOL_PREFIX
import com.jetbrains.python.packaging.PyPackageName
import com.jetbrains.python.packaging.PyRequirementParser
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.VisibleForTesting
import org.toml.lang.psi.*
private val toolsWhiteList = listOf(
"autoflake",
"basedpyright",
"black",
"cibuildwheel",
"cmake",
"codespell",
"comfy",
"conan",
"conda-lock",
"coverage",
"cython",
"flake8",
"flit",
"flit-core",
"hatch",
"hatch-vcs",
"hatchling",
"isort",
"make-env",
"mypy",
"ninja",
"nitpick",
"pdm",
"poe",
"poetry",
"poetry-core",
"pybind11",
"pycln",
"pydantic-mypy",
"pylint",
"pyright",
"pytest",
"pytoniq",
"refurb",
"ruff",
"scikit-build",
"sematic-release",
"setuptools",
"setuptools-rust",
"setuptools-scm",
"vulture",
"wheel",
)
@Internal
@VisibleForTesting
class PyProjectTomlUsageCollector : ProjectUsagesCollector() {
private val GROUP = EventLogGroup("python.toml.stats", 1)
private val PYTHON_PYTOML_TOOLS = GROUP.registerEvent(
"python.pyproject.tools",
EventFields.String("name", toolsWhiteList),
)
// https://peps.python.org/pep-0518/
private val PYTHON_BUILD_BACKEND = GROUP.registerEvent(
"python.pyproject.buildsystem",
EventFields.String("name", toolsWhiteList),
)
override fun getGroup(): EventLogGroup = GROUP
override fun requiresReadAccess() = true
override fun requiresSmartMode() = true
override fun getMetrics(project: Project): Set<MetricEvent> {
val tools = mutableSetOf<String>()
val buildSystems = mutableSetOf<String>()
FileTypeIndex.processFiles(
TomlFileType,
{ file: VirtualFile ->
val psiFile = file.findPsiFile(project)
if (file.name == PY_PROJECT_TOML && psiFile != null && psiFile.isValid) {
collectTools(psiFile, tools)
collectBuildBackends(psiFile, buildSystems)
}
return@processFiles true
},
ProjectScope.getContentScope(project))
val metrics = mutableSetOf<MetricEvent>()
tools.forEach { name ->
metrics.add(PYTHON_PYTOML_TOOLS.metric(name))
}
buildSystems.forEach { name: String ->
metrics.add(PYTHON_BUILD_BACKEND.metric(name))
}
return metrics
}
companion object {
const val BUILD_REQUIRES = "requires"
@JvmStatic
fun collectTools(file: PsiFile, tools: MutableSet<String>) {
val collected = file.children.map { element ->
val key = (element as? TomlTable)?.header?.key?.text ?: ""
val name = if (key.startsWith(PY_PROJECT_TOML_TOOL_PREFIX))
key.substringAfter(PY_PROJECT_TOML_TOOL_PREFIX, "").substringBefore(".")
else ""
PyPackageName.normalizePackageName(name)
}.filter {
it.isNotEmpty()
}
tools.addAll(collected)
}
@JvmStatic
fun collectBuildBackends(file: PsiFile, systems: MutableSet<String>) {
val collected = file.children
.filter { element ->
(element as? TomlTable)?.header?.key?.text == PY_PROJECT_TOML_BUILD_SYSTEM
}.flatMap { it ->
it.children.filter { line ->
val kv = (line as? TomlKeyValue)
kv?.key?.text == BUILD_REQUIRES && kv.value as? TomlArray != null
}.flatMap { line ->
val array = (line as TomlKeyValue).value as TomlArray
array.elements.mapNotNull {
(it as? TomlLiteral)?.text
}
}
}.mapNotNull {
val requirement = PyRequirementParser.fromLine(PyPackageName.normalizePackageName(it))
requirement?.name
}
systems.addAll(collected)
}
}
}