Skip to content

Commit c24c263

Browse files
Timur Malaninintellij-monorepo-bot
authored andcommitted
PY-82848 Refactor package handling to unify installed and requirement package processing in search results.
GitOrigin-RevId: c2b8ed89e4294d5607f66a2dede2f5cf3f5367fa
1 parent 0be71e9 commit c24c263

File tree

7 files changed

+90
-40
lines changed

7 files changed

+90
-40
lines changed

python/src/com/jetbrains/python/packaging/packageRequirements/PythonPackageRequirementsTreeExtractor.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
package com.jetbrains.python.packaging.packageRequirements
33

44
import com.intellij.openapi.extensions.ExtensionPointName
5-
import com.intellij.openapi.module.Module
65
import com.intellij.openapi.projectRoots.Sdk
76
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
87
import com.jetbrains.python.packaging.common.PythonPackage
98
import org.jetbrains.annotations.ApiStatus
109

1110
@ApiStatus.Internal
1211
interface PythonPackageRequirementsTreeExtractor {
13-
suspend fun extract(pkg: PythonPackage, module: Module): PackageNode
12+
suspend fun extract(pkg: PythonPackage): PackageNode
1413

1514
companion object {
1615
private val treeParser = TreeParser()

python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ class PyPackagingToolWindowPanel(private val project: Project) : SimpleToolWindo
228228
descriptionController.setPackage(selectedPackage)
229229
}
230230

231-
fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
231+
fun showSearchResult(installed: List<DisplayablePackage>, repoData: List<PyPackagesViewData>) {
232232
packageListController.showSearchResult(installed, repoData)
233233
}
234234

python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import com.intellij.openapi.components.Service
1010
import com.intellij.openapi.components.service
1111
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
1212
import com.intellij.openapi.fileEditor.FileEditorManagerListener
13-
import com.intellij.openapi.module.Module
1413
import com.intellij.openapi.module.ModuleUtilCore
1514
import com.intellij.openapi.options.ex.SingleConfigurableEditor
1615
import com.intellij.openapi.project.Project
@@ -39,6 +38,7 @@ import com.jetbrains.python.packaging.toolwindow.model.*
3938
import com.jetbrains.python.sdk.PythonSdkUtil
4039
import com.jetbrains.python.sdk.pythonSdk
4140
import kotlinx.coroutines.*
41+
import org.jetbrains.annotations.ApiStatus
4242
import org.jetbrains.annotations.Nls
4343

4444
@Service(Service.Level.PROJECT)
@@ -87,6 +87,68 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
8787
}
8888
}
8989

90+
private fun nameMatches(pkg: DisplayablePackage, query: String): Boolean {
91+
val shouldUseStraightComparison = when (pkg) {
92+
is InstalledPackage -> isNonPipCondaPackage(pkg.instance)
93+
is RequirementPackage -> isNonPipCondaPackage(pkg.instance)
94+
else -> false
95+
}
96+
97+
return if (shouldUseStraightComparison) {
98+
StringUtil.containsIgnoreCase(pkg.name, query)
99+
} else {
100+
StringUtil.containsIgnoreCase(normalizePackageName(pkg.name), normalizePackageName(query))
101+
}
102+
}
103+
104+
private fun isNonPipCondaPackage(pkg: PythonPackage): Boolean = pkg is CondaPackage && !pkg.installedWithPip
105+
106+
private fun traversePackageTree(
107+
pkg: DisplayablePackage,
108+
visited: MutableSet<String>,
109+
matches: MutableList<RequirementPackage>,
110+
query: String,
111+
) {
112+
if (!visited.add(pkg.name)) return
113+
114+
if (pkg is RequirementPackage && nameMatches(pkg, query)) {
115+
matches.add(pkg)
116+
}
117+
118+
for (requirementPackage in pkg.getRequirements()) {
119+
traversePackageTree(requirementPackage, visited, matches, query)
120+
}
121+
}
122+
123+
/**
124+
* Finds all packages (both installed and requirements) that match the given query.
125+
*/
126+
@ApiStatus.Internal
127+
fun findAllMatchingPackages(query: String): List<DisplayablePackage> {
128+
val matchingInstalled = installedPackages.values.filter { nameMatches(it, query) }
129+
val matchingRequirements = mutableListOf<RequirementPackage>()
130+
val visited = mutableSetOf<String>()
131+
132+
for (pkg in installedPackages.values) {
133+
traversePackageTree(pkg, visited, matchingRequirements, query)
134+
}
135+
136+
return unifyPackages(matchingInstalled, matchingRequirements)
137+
}
138+
139+
/**
140+
* Unifies packages with the same name according to the following rules:
141+
* 1. If both an installed package and a requirement package have the same name, keep only the installed package.
142+
* 2. If multiple requirement packages have the same name, keep only one of them.
143+
*/
144+
private fun unifyPackages(installedPackages: List<InstalledPackage>, requirementPackages: List<RequirementPackage>): List<DisplayablePackage> {
145+
return (installedPackages + requirementPackages)
146+
.groupBy { it.name.lowercase() }
147+
.map { (_, packages) ->
148+
packages.find { it is InstalledPackage } ?: packages.first()
149+
}
150+
}
151+
90152
fun handleSearch(query: String) {
91153
val manager = manager ?: return
92154
val prevSelected = toolWindowPanel?.getSelectedPackage()
@@ -95,21 +157,14 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
95157
if (query.isNotEmpty()) {
96158
searchJob?.cancel()
97159
searchJob = serviceScope.launch {
98-
val installed = installedPackages.values.filter { pkg ->
99-
when {
100-
pkg.instance is CondaPackage && !pkg.instance.installedWithPip -> StringUtil.containsIgnoreCase(pkg.name, query)
101-
else -> StringUtil.containsIgnoreCase(normalizePackageName(pkg.name), normalizePackageName(query))
102-
}
103-
}
104-
105-
106-
val packagesFromRepos = manager.repositoryManager.searchPackages(query).map {
107-
sortPackagesForRepo(it.value, query, it.key)
108-
}.toList()
160+
val allMatches = findAllMatchingPackages(query)
161+
val packagesFromRepos = manager.repositoryManager.searchPackages(query)
162+
.map { (repository, packages) -> sortPackagesForRepo(packages, query, repository) }
163+
.toList()
109164

110165
if (isActive) {
111166
withContext(Dispatchers.Main) {
112-
toolWindowPanel?.showSearchResult(installed, packagesFromRepos + invalidRepositories)
167+
toolWindowPanel?.showSearchResult(allMatches, packagesFromRepos + invalidRepositories)
113168
prevSelected?.name?.let { toolWindowPanel?.selectPackageName(it) }
114169
}
115170
}
@@ -147,7 +202,8 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
147202
handleActionCompleted(message("python.packaging.notification.deleted", selectedPackages.joinToString(", ") { it.name }))
148203
}
149204

150-
internal suspend fun initForSdk(sdk: Sdk?) {
205+
@ApiStatus.Internal
206+
suspend fun initForSdk(sdk: Sdk?) {
151207
if (sdk == null) {
152208
toolWindowPanel?.packageListController?.setLoadingState(false)
153209
}
@@ -234,7 +290,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
234290

235291
suspend fun refreshInstalledPackages() {
236292
val sdk = currentSdk ?: return
237-
val targetModule = findTargetModule(sdk) ?: return
238293
val manager = manager ?: return
239294
withContext(Dispatchers.Default) {
240295
val declaredPackages = manager.reloadDependencies()
@@ -245,9 +300,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
245300
processPackagesWithRequirementsTree(
246301
installedDeclaredPackages,
247302
treeExtractor,
248-
targetModule
249303
)
250-
} else {
304+
}
305+
else {
251306
emptyList()
252307
}
253308

@@ -262,9 +317,6 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
262317
}
263318
}
264319

265-
private fun findTargetModule(sdk: Sdk): Module? =
266-
project.modules.find { it.pythonSdk == sdk }
267-
268320
private suspend fun findInstalledDeclaredPackages(declaredPackages: List<PythonPackage>): List<PythonPackage> =
269321
manager?.listInstalledPackages()?.filter {
270322
it.name in declaredPackages.map { pkg -> pkg.name }
@@ -273,10 +325,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
273325
private suspend fun processPackagesWithRequirementsTree(
274326
packages: List<PythonPackage>,
275327
treeExtractor: PythonPackageRequirementsTreeExtractor,
276-
targetModule: Module,
277328
): List<InstalledPackage> {
278329
return packages.mapNotNull { pkg ->
279-
val tree = treeExtractor.extract(pkg, targetModule)
330+
val tree = treeExtractor.extract(pkg)
280331
createInstalledPackageFromTree(pkg, tree)
281332
}
282333
}

python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTreeView.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ internal class PyPackagingTreeView(
5252
}
5353
}
5454

55-
fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
55+
fun showSearchResult(installed: List<DisplayablePackage>, repoData: List<PyPackagesViewData>) {
5656
updatePackages(installed, repoData)
5757

5858
installedPackages.expand()
@@ -94,7 +94,7 @@ internal class PyPackagingTreeView(
9494
container.scrollRectToVisible(Rectangle(0, 0))
9595
}
9696

97-
private fun updatePackages(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
97+
private fun updatePackages(installed: List<DisplayablePackage>, repoData: List<PyPackagesViewData>) {
9898
val sortedInstalled = installed.sortedBy { it.name }
9999
installedPackages.tree.items = sortedInstalled
100100
updateExistingRepository(installedPackages, sortedInstalled)

python/src/com/jetbrains/python/packaging/toolwindow/packages/PyPackagesListController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal class PyPackagesListController(val project: Project, val controller: Py
5050

5151
override fun dispose() {}
5252

53-
fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
53+
fun showSearchResult(installed: List<DisplayablePackage>, repoData: List<PyPackagesViewData>) {
5454
tablesView.showSearchResult(installed, repoData)
5555
setLoadingState(false)
5656
}

python/src/com/jetbrains/python/poetry/packaging/PoetryPackageRequirementsTreeExtractor.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package com.jetbrains.python.poetry.packaging
33

44
import com.intellij.openapi.diagnostic.thisLogger
5-
import com.intellij.openapi.module.Module
65
import com.intellij.openapi.projectRoots.Sdk
76
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
87
import com.jetbrains.python.packaging.common.PythonPackage
@@ -18,7 +17,7 @@ import com.jetbrains.python.sdk.poetry.runPoetryWithSdk
1817
*/
1918
internal class PoetryPackageRequirementsTreeExtractor(private val sdk: Sdk) : PythonPackageRequirementsTreeExtractor {
2019

21-
override suspend fun extract(pkg: PythonPackage, module: Module): PackageNode {
20+
override suspend fun extract(pkg: PythonPackage): PackageNode {
2221
val data = runPoetryWithSdk(sdk, "show", "--tree", pkg.name).getOr {
2322
thisLogger().info("extracting requirements for package ${pkg.name}: error. Output: \n${it.error}")
2423
return PackageNode(NormalizedPythonPackageName.from(pkg.name))

python/src/com/jetbrains/python/uv/packaging/UvPackageRequirementsTreeExtractor.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22
package com.jetbrains.python.uv.packaging
33

44
import com.intellij.openapi.diagnostic.thisLogger
5-
import com.intellij.openapi.module.Module
65
import com.intellij.openapi.projectRoots.Sdk
76
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
87
import com.jetbrains.python.packaging.common.PythonPackage
98
import com.jetbrains.python.packaging.packageRequirements.PackageNode
109
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor
1110
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractor.Companion.parseTree
1211
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementsTreeExtractorProvider
13-
import com.jetbrains.python.sdk.basePath
1412
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
1513
import com.jetbrains.python.sdk.uv.impl.createUvCli
1614
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
@@ -19,23 +17,26 @@ import java.nio.file.Path
1917

2018
internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequirementsTreeExtractor {
2119

22-
override suspend fun extract(pkg: PythonPackage, module: Module): PackageNode {
23-
val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.basePath!!)
24-
val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
25-
val out = uv.listPackageRequirementsTree(pkg).getOr {
26-
thisLogger().info("extracting requires for package ${pkg.name}: error. Output: \n${it.error}")
27-
return PackageNode(NormalizedPythonPackageName.from(pkg.name), mutableListOf())
20+
override suspend fun extract(pkg: PythonPackage): PackageNode {
21+
val workingDir = uvWorkingDirectory ?: return createLeafPackageNode(pkg.name)
22+
val uvInstance = createUvLowLevel(workingDir, createUvCli())
23+
val requirementsOutput = uvInstance.listPackageRequirementsTree(pkg).getOr {
24+
thisLogger().info("extracting requires for package $pkg.name: error. Output: \n${it.error}")
25+
return createLeafPackageNode(pkg.name)
2826
}
2927

30-
return parseTree(out.lines())
28+
return parseTree(requirementsOutput.lines())
3129
}
30+
31+
private fun createLeafPackageNode(packageName: String): PackageNode =
32+
PackageNode(NormalizedPythonPackageName.from(packageName), mutableListOf())
3233
}
3334

3435

3536
private class UvPackageRequirementsTreeExtractorProvider : PythonPackageRequirementsTreeExtractorProvider {
3637
override fun createExtractor(sdk: Sdk): PythonPackageRequirementsTreeExtractor? {
3738
if (!sdk.isUv) return null
3839
val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
39-
return UvPackageRequirementsTreeExtractor( data.uvWorkingDirectory)
40+
return UvPackageRequirementsTreeExtractor(data.uvWorkingDirectory)
4041
}
4142
}

0 commit comments

Comments
 (0)