PY-54850 Normalized package names in PyPackageRequirementsInspection for consistent matching and refactored requirement satisfaction checks. Unified normalizePackageName logic. introduced a test to verify that requirement mismatch warnings disappear upon package installation.

GitOrigin-RevId: edb02fa9c575b3cc51d95bbe21bf5cd2f0d25cba
This commit is contained in:
Timur Malanin
2025-02-27 16:12:15 +00:00
committed by intellij-monorepo-bot
parent cf32cfa669
commit a7bdd63a2e
23 changed files with 159 additions and 86 deletions

View File

@@ -58,9 +58,11 @@ public interface PyRequirement {
}
/**
* @return concatenated representation of name, extras and version specs so it could be easily displayed.
* @return concatenated representation of name, extras and version specs, so it could be easily displayed.
*/
default @NotNull @NlsSafe String getPresentableText() {
return getName() + getExtras() + StringUtil.join(getVersionSpecs(), PyRequirementVersionSpec::getPresentableText, ",");
return getPresentableTextWithoutVersion() + getExtras() + StringUtil.join(getVersionSpecs(), PyRequirementVersionSpec::getPresentableText, ",");
}
@NotNull @NlsSafe String getPresentableTextWithoutVersion();
}

View File

@@ -0,0 +1,13 @@
package com.jetbrains.python.packaging
private val QUOTES_REGEX: Regex = Regex("^\"|\"$")
private val SEPARATOR_REGEX: Regex = Regex("[-_.]+")
/**
* Normalizes a package name by removing quotes, replacing separators, and converting to lowercase.
*/
fun normalizePackageName(name: String): String = name
.replace(QUOTES_REGEX, "")
.replace(SEPARATOR_REGEX, "-")
.lowercase()

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
/**
@@ -13,20 +14,45 @@ import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
* @see PyRequirementParser.fromLine
* @see PyRequirementParser.fromFile
*/
data class PyRequirementImpl(private val name: String,
private val versionSpecs: List<PyRequirementVersionSpec>,
private val installOptions: List<String>,
private val extras: String) : PyRequirement {
data class PyRequirementImpl(
private val name: String,
private val versionSpecs: List<PyRequirementVersionSpec>,
private val installOptions: List<String>,
private val extras: String,
) : PyRequirement {
override fun getName(): String = name
override fun getName(): String = NormalizedPackageName.from(name).name
override fun getExtras(): String = extras
override fun getVersionSpecs(): List<PyRequirementVersionSpec> = versionSpecs
override fun getInstallOptions(): List<String> = installOptions
override fun getPresentableTextWithoutVersion(): @NlsSafe String = name
override fun match(packages: Collection<PyPackage>): PyPackage? {
return packages.firstOrNull { `package` ->
name.replace('_', '-').equals(`package`.name.replace('_', '-'), true)
&& versionSpecs.all { it.matches(`package`.version) }
return packages.firstOrNull { pkg ->
isPackageNameEqual(pkg.name) && versionSpecs.all { it.matches(pkg.version) }
}
}
}
private fun isPackageNameEqual(otherName: String): Boolean {
return normalizePackageName(name) == normalizePackageName(otherName)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return when (other) {
is String -> getName() == NormalizedPackageName.from(other).name
is PyRequirementImpl -> getName().equals(other.getName(), ignoreCase = true)
else -> false
}
}
override fun hashCode(): Int = normalizePackageName(name).hashCode()
@JvmInline
value class NormalizedPackageName private constructor(val name: String) {
companion object {
fun from(name: String): NormalizedPackageName =
NormalizedPackageName(normalizePackageName(name))
}
}
}

View File

@@ -40,7 +40,7 @@ class PyStubPackagesCompatibilityInspection : PyInspection() {
.mapNotNull { stubPkg -> nameToPkg[stubPkg.name.removeSuffix(STUBS_SUFFIX)]?.let { it to stubPkg } }
.filter {
val runtimePkgName = it.first.name
val requirement = it.second.requirements.firstOrNull { req -> req.name == runtimePkgName } ?: return@filter false
val requirement = it.second.requirements.firstOrNull { req -> req.equals(runtimePkgName) } ?: return@filter false
requirement.match(listOf(it.first)) == null
}
@@ -84,7 +84,7 @@ class PyStubPackagesCompatibilityInspection : PyInspection() {
}
.forEach { (runtimePkg, stubPkg) ->
val runtimePkgName = runtimePkg.name
val requirement = stubPkg.requirements.firstOrNull { it.name == runtimePkgName } ?: return@forEach
val requirement = stubPkg.requirements.firstOrNull { it.equals(runtimePkgName) } ?: return@forEach
if (requirement.match(listOf(runtimePkg)) == null) {
val stubPkgName = stubPkg.name

View File

@@ -17,10 +17,7 @@ import com.jetbrains.python.inspections.PyInspectionVisitor
import com.jetbrains.python.inspections.quickfix.IgnoreRequirementFix
import com.jetbrains.python.inspections.quickfix.PyGenerateRequirementsFileQuickFix
import com.jetbrains.python.inspections.quickfix.PyInstallRequirementsFix
import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.PyPackage
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.psi.*
@@ -65,7 +62,7 @@ class PyRequirementVisitor(
.firstOrNull()
add(providedFix ?: PyInstallRequirementsFix(null, module, sdk, unsatisfied))
add(IgnoreRequirementFix(unsatisfied.mapTo(mutableSetOf()) { it.name }))
add(IgnoreRequirementFix(unsatisfied.mapTo(mutableSetOf()) { it.presentableTextWithoutVersion }))
}
registerProblem(
@@ -91,7 +88,7 @@ class PyRequirementVisitor(
ignoredPackages: Set<String?>,
): List<PyRequirement> {
val requirements = getRequirements(module) ?: return emptyList()
val installedPackages = manager.installedPackages.toPyPackages()
val installedPackages = manager.installedPackages
val modulePackages = collectPackagesInModule(module)
return requirements.filter { requirement ->
@@ -102,19 +99,23 @@ class PyRequirementVisitor(
private fun isRequirementUnsatisfied(
requirement: PyRequirement,
ignoredPackages: Set<String?>,
installedPackages: List<PyPackage>,
modulePackages: List<PyPackage>,
): Boolean =
!ignoredPackages.contains(requirement.name) &&
requirement.match(installedPackages) == null &&
requirement.match(modulePackages) == null
installedPackages: List<PythonPackage>,
modulePackages: List<PythonPackage>,
): Boolean {
if (requirement.name in ignoredPackages.map { normalizePackageName(it ?: EMPTY_STRING) }) {
return false
}
private fun List<PythonPackage>.toPyPackages(): List<PyPackage> = map { PyPackage(it.name, it.version) }
val isSatisfiedInInstalled = requirement.match(installedPackages) != null
val isSatisfiedInModule = requirement.match(modulePackages) != null
return !(isSatisfiedInInstalled || isSatisfiedInModule)
}
private fun getRequirements(module: Module): List<PyRequirement>? =
PyPackageUtil.getRequirementsFromTxt(module) ?: PyPackageUtil.findSetupPyRequires(module)
private fun collectPackagesInModule(module: Module): List<PyPackage> {
private fun collectPackagesInModule(module: Module): List<PythonPackage> {
return PyUtil.getSourceRoots(module).flatMap { srcRoot ->
VfsUtil.getChildren(srcRoot).filter { file ->
METADATA_EXTENSIONS.contains(file.extension)
@@ -124,9 +125,9 @@ class PyRequirementVisitor(
}
}
private fun parsePackageNameAndVersion(nameWithoutExtension: String): PyPackage? {
private fun parsePackageNameAndVersion(nameWithoutExtension: String): PythonPackage? {
val components = splitNameIntoComponents(nameWithoutExtension)
return if (components.size >= 2) PyPackage(components[0], components[1]) else null
return if (components.size >= 2) PythonPackage(components[0], components[1], false) else null
}
private fun checkPackageNameInRequirements(importedExpression: PyQualifiedExpression) {
@@ -174,14 +175,8 @@ class PyRequirementVisitor(
possiblePyPIPackageNames: String,
requirements: Collection<PyRequirement>,
): Boolean =
requirements.map { it.name.variations() }.flatten().contains(packageName) ||
requirements.map { it.name.variations() }.flatten().contains(possiblePyPIPackageNames)
private fun String.variations() = listOf(
this,
this.replace("_", "-"),
this.replace("-", "_")
)
requirements.map { it.name }.contains(normalizePackageName(packageName)) ||
requirements.map { it.name }.contains(normalizePackageName(possiblePyPIPackageNames))
private fun isLocalModule(packageReferenceExpression: PyExpression, module: Module): Boolean {
val reference = packageReferenceExpression.reference ?: return false
@@ -242,6 +237,15 @@ class PyRequirementVisitor(
return value != null && value
}
private fun PyRequirement.match(packages: Collection<PythonPackage>): PythonPackage? {
return packages.firstOrNull { pkg ->
name == pkg.name
&& versionSpecs.all { it.matches(pkg.version) }
}
}
private fun List<PythonPackage>.toPyPackages(): List<PyPackage> = map { PyPackage(it.name, it.version) }
companion object {
private const val PACKAGE_NOT_LISTED = "INSP.requirements.package.containing.module.not.listed.in.project.requirements"
private const val REQUIREMENT_NOT_SATISFIED = "INSP.requirements.package.requirements.not.satisfied"

View File

@@ -31,7 +31,7 @@ import com.jetbrains.python.inspections.PyUnresolvedReferenceQuickFixProvider
import com.jetbrains.python.inspections.quickfix.*
import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.common.normalizePackageName
import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyFromImportStatementImpl
import com.jetbrains.python.psi.impl.PyImportElementImpl

View File

@@ -41,7 +41,8 @@ import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static com.jetbrains.python.packaging.common.PackagesKt.normalizePackageName;
import static com.jetbrains.python.packaging.PyPackageNameNormalizeUtilKt.normalizePackageName;
@ApiStatus.Internal
public final class PyPIPackageUtil {

View File

@@ -15,7 +15,6 @@ import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.normalizePackageName
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.ui.PyChooseRequirementsDialog
import com.jetbrains.python.statistics.PyPackagesUsageCollector
@@ -138,7 +137,7 @@ internal fun getConfirmedPackages(packageNames: List<PyRequirement>, project: Pr
if (!confirmationEnabled || packageNames.isEmpty()) return packageNames.toSet()
val dialog = PyChooseRequirementsDialog(project, packageNames) { it.presentableText }
val dialog = PyChooseRequirementsDialog(project, packageNames) { it.presentableTextWithoutVersion }
if (!dialog.showAndGet()) {
PyPackagesUsageCollector.installAllCanceledEvent.log()

View File

@@ -5,7 +5,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.jetbrains.python.PyBundle
import java.util.Locale
import java.util.*
class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String, PyPackage>,
private val settings: PyPackageRequirementsSettings) {
@@ -75,7 +75,7 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
}
else {
val requirement = parsed.first()
val name = requirement.name.lowercase(Locale.getDefault())
val name = requirement.name
if (name in importedPackages) {
val pkg = importedPackages.remove(name)!!
val formatted = formatRequirement(requirement, pkg, lines)
@@ -131,10 +131,10 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
private fun convertToRequirementsEntry(requirement: PyRequirement, settings: PyPackageRequirementsSettings, version: String? = null): String {
val packageName = when {
settings.specifyVersion -> when {
version != null -> requirement.name + requirement.extras + settings.versionSpecifier.separator + version
else -> requirement.presentableText
version != null -> requirement.presentableTextWithoutVersion + requirement.extras + settings.versionSpecifier.separator + version
else -> requirement.presentableTextWithoutVersion
}
else -> requirement.name + requirement.extras
else -> requirement.presentableTextWithoutVersion + requirement.extras
}
if (requirement.installOptions.size == 1) return packageName

View File

@@ -2,17 +2,21 @@
package com.jetbrains.python.packaging.common
import com.intellij.openapi.diagnostic.thisLogger
import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.packaging.repository.PyEmptyPackagePackageRepository
import com.jetbrains.python.packaging.repository.PyPIPackageRepository
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import org.jetbrains.annotations.Nls
open class PythonPackage(val name: String, val version: String, val isEditableMode: Boolean) {
open class PythonPackage(name: String, val version: String, val isEditableMode: Boolean) {
companion object {
private const val HASH_MULTIPLIER = 31
}
val name: String = normalizePackageName(name)
val presentableName: String = name
override fun toString(): String {
return "PythonPackage(name='$name', version='$version')"
}
@@ -128,8 +132,4 @@ data class PythonLocalPackageSpecification(override val name: String,
data class PythonVcsPackageSpecification(override val name: String,
override val location: String,
override val prefix: String,
override val editable: Boolean) : PythonLocationBasedPackageSpecification
fun normalizePackageName(name: String): String {
return name.replace(Regex("[-_.]+"), "-").lowercase()
}
override val editable: Boolean) : PythonLocationBasedPackageSpecification

View File

@@ -23,8 +23,8 @@ import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.normalizePackageName
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.buildTargetedCommandLine

View File

@@ -10,12 +10,12 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.io.HttpRequests
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPIPackageUtil
import com.jetbrains.python.packaging.PyPackageVersion
import com.jetbrains.python.packaging.PyPackageVersionComparator
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.cache.PythonSimpleRepositoryCache
import com.jetbrains.python.packaging.common.*
import com.jetbrains.python.packaging.common.EmptyPythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import com.jetbrains.python.packaging.management.PythonRepositoryManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.repository.*

View File

@@ -23,6 +23,10 @@ import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.common.*
import com.jetbrains.python.packaging.conda.CondaPackage
import com.jetbrains.python.packaging.management.PythonPackageManager

View File

@@ -14,10 +14,10 @@ import javax.swing.Icon
sealed class DisplayablePackage(@NlsSafe val name: String, val repository: PyPackageRepository)
class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository, val nextVersion: PyPackageVersion? = null) : DisplayablePackage(instance.name, repository) {
val currentVersion = PyPackageVersionNormalizer.normalize(instance.version)
class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository, val nextVersion: PyPackageVersion? = null) : DisplayablePackage(instance.presentableName, repository) {
val currentVersion: PyPackageVersion? = PyPackageVersionNormalizer.normalize(instance.version)
val isEditMode = instance.isEditableMode
val isEditMode: Boolean = instance.isEditableMode
val sourceRepoIcon: Icon?
get() {
val condaPackage = instance as? CondaPackage ?: return null

View File

@@ -26,7 +26,6 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.normalizePackageName
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.createSpecification
@@ -100,7 +99,7 @@ class InstallAllRequirementsQuickFix(requirements: List<Requirement>) : LocalQui
InstallRequirementQuickFix.installPackages(
project,
descriptor,
requirementElements.filter { pkg -> confirmedPackages.any { it.name == pkg.displayName } }
requirementElements.filter { pkg -> confirmedPackages.any { it.equals(pkg.displayName) } }
)
}

View File

@@ -8,6 +8,7 @@ import com.intellij.internal.statistic.service.fus.collectors.ProjectUsagesColle
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.project.Project
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.packaging.normalizePackageName
class PyPackageInEditorUsageCollector : ProjectUsagesCollector() {
override fun getMetrics(project: Project): Set<MetricEvent> {

View File

@@ -11,6 +11,7 @@ import com.jetbrains.python.extensions.getSdk
import com.jetbrains.python.packaging.PyPIPackageCache
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.normalizePackageName
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkUtil

View File

@@ -12,6 +12,7 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.ProjectScope
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.normalizePackageName
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.VisibleForTesting
import org.toml.lang.psi.*

View File

@@ -20,7 +20,6 @@ import com.jetbrains.python.sdk.PySdkUtil
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.venvReader.VirtualEnvReader
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
import com.jetbrains.python.sdk.pipenv.isPipEnv
import com.jetbrains.python.sdk.poetry.isPoetry
@@ -28,6 +27,7 @@ import com.jetbrains.python.statistics.InterpreterCreationMode.*
import com.jetbrains.python.statistics.InterpreterTarget.*
import com.jetbrains.python.statistics.InterpreterType.*
import com.jetbrains.python.target.PyTargetAwareAdditionalData
import com.jetbrains.python.venvReader.VirtualEnvReader
val Project.modules get() = ModuleManager.getInstance(this).modules
val Project.sdks get() = modules.mapNotNull(Module::getSdk)
@@ -52,19 +52,6 @@ fun getPythonSpecificInfo(sdk: Sdk): List<EventPair<*>> {
return data
}
fun normalizePackageName(packageName: String): String {
var name = packageName
if (!name.startsWith("_")) {
// for cases such as __future__, etc
name = name.replace('_', '-')
}
return name
.replace(".", "-")
.replace("\"", "")
.lowercase()
}
@Deprecated("""
It makes no sense to add a Python version or something similar to the event.
If you need to get an event with a specific execution type, interpreter type, or whatsoever, please use the corresponding segment in the analytics platform.

View File

@@ -7,6 +7,7 @@ import com.intellij.testFramework.ServiceContainerUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.packaging.PyRequirement;
import com.jetbrains.python.packaging.common.PythonPackage;
import com.jetbrains.python.packaging.management.PythonPackageManagerService;
import com.jetbrains.python.packaging.management.TestPythonPackageManagerService;
import com.jetbrains.python.psi.LanguageLevel;
@@ -14,8 +15,10 @@ import com.jetbrains.python.sdk.PythonSdkUtil;
import com.jetbrains.python.sdk.pipenv.PipenvFilesUtilsKt;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
@NotNull
@Override
@@ -23,18 +26,23 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
return PyPackageRequirementsInspection.class;
}
private void replacePythonPackageManagerServiceWithTestInstance(
PythonPackageManagerService serviceInstance) {
ServiceContainerUtil.replaceService(
myFixture.getProject(),
PythonPackageManagerService.class,
serviceInstance,
myFixture.getProject()
);
}
@Override
public void setUp() throws Exception {
super.setUp();
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
assertNotNull(sdk);
ServiceContainerUtil.replaceService(
myFixture.getProject(),
PythonPackageManagerService.class,
new TestPythonPackageManagerService(),
myFixture.getProject()
);
replacePythonPackageManagerServiceWithTestInstance(new TestPythonPackageManagerService());
}
public void testPartiallySatisfiedRequirementsTxt() {
@@ -116,4 +124,15 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
myFixture.enableInspections(inspection);
myFixture.checkHighlighting(isWarning(), isInfo(), isWeakWarning());
}
}
// PY-54850
public void testRequirementMismatchWarningDisappearsOnInstall() {
PythonPackage zopeInterfacePackage = new PythonPackage("zope.interface", "5.4.0", false);
replacePythonPackageManagerServiceWithTestInstance(
new TestPythonPackageManagerService(Collections.singletonList(zopeInterfacePackage))
);
doMultiFileTest("a.py");
}
}

View File

@@ -7,6 +7,8 @@ import com.jetbrains.python.packaging.bridge.PythonPackageManagementServiceBridg
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import com.jetbrains.python.packaging.repository.PyEmptyPackagePackageRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import org.jetbrains.annotations.TestOnly
@@ -63,6 +65,11 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
return this
}
fun withPackageInstalled(packages: List<PythonPackage>): TestPythonPackageManager {
this.installedPackages = packages
return this
}
companion object {
private val DEFAULT_PACKAGES = listOf(
PythonPackage(PIP_PACKAGE, EMPTY_STRING, false),
@@ -78,10 +85,17 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
}
@TestOnly
class TestPythonPackageManagerService(): PythonPackageManagerService {
class TestPythonPackageManagerService(val installedPackages: List<PythonPackage> = emptyList()): PythonPackageManagerService {
override fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
installedPackages.ifEmpty {
return TestPythonPackageManager(project, sdk)
}
return TestPythonPackageManager(project, sdk)
.withPackageInstalled(installedPackages)
.withPackageNames(installedPackages.map { it.name })
.withPackageDetails(PythonSimplePackageDetails(installedPackages.first().name, listOf(installedPackages.first().version),PyEmptyPackagePackageRepository))
}
override fun bridgeForSdk(project: Project, sdk: Sdk): PythonPackageManagementServiceBridge {
@@ -97,6 +111,7 @@ class TestPythonPackageManagerService(): PythonPackageManagerService {
class TestPackageManagerProvider : PythonPackageManagerProvider {
private var packageNames: List<String> = emptyList()
private var packageDetails: PythonPackageDetails? = null
private var packageInstalled: List<PythonPackage> = emptyList()
fun withPackageNames(packageNames: List<String>): TestPackageManagerProvider {
this.packageNames = packageNames
@@ -109,6 +124,6 @@ class TestPackageManagerProvider : PythonPackageManagerProvider {
}
override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager {
return TestPythonPackageManager(project, sdk).withPackageNames(packageNames).withPackageDetails(packageDetails)
return TestPythonPackageManager(project, sdk).withPackageNames(packageNames).withPackageDetails(packageDetails).withPackageInstalled(packageInstalled)
}
}