PY-55589/PY-65967 Implement support for partial stubs

GitOrigin-RevId: eae99e99b99d7d52b4b7f6851bef2fe868e80f1a
This commit is contained in:
evgeny.bovykin
2026-06-12 07:54:14 +00:00
committed by intellij-monorepo-bot
parent a5c09533c6
commit a30073abff
102 changed files with 734 additions and 19 deletions
@@ -160,6 +160,7 @@
<pluginSuggestionProvider order="first" implementation="com.jetbrains.python.suggestions.PycharmProSuggestionProvider"/>
<postStartupActivity implementation="com.jetbrains.python.poetry.PoetryPyProjectTomlPostStartupActivity"/>
<postStartupActivity implementation="com.jetbrains.python.psi.resolve.PyPartialStubMarkersInvalidatorActivity"/>
<virtualFileCustomDataProvider implementation="com.jetbrains.python.psi.PyLangLevelVirtualFileCustomDataProvider"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.run.features.PyRunToolUsageCollector"/>
+4
View File
@@ -66,6 +66,8 @@ jvm_library(
"//libraries/hdr-histogram",
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable",
"//python/python-requirements:requirements",
"//plugins/toml",
"//plugins/toml/core",
],
)
@@ -128,6 +130,8 @@ jvm_library(
"//libraries/hdr-histogram:hdr-histogram_test_lib",
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable_test_lib",
"//python/python-requirements:requirements_test_lib",
"//plugins/toml:toml_test_lib",
"//plugins/toml/core:core_test_lib",
],
)
### auto-generated section `build intellij.python.psi.impl` end
@@ -52,5 +52,7 @@
<orderEntry type="module" module-name="intellij.libraries.hdr.histogram" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.collections.immutable" />
<orderEntry type="module" module-name="intellij.python.requirements" />
<orderEntry type="module" module-name="intellij.toml" />
<orderEntry type="module" module-name="intellij.toml.core" />
</component>
</module>
@@ -1,10 +1,14 @@
<idea-plugin visibility="public">
<dependencies>
<module name="intellij.python.psi"/>
<module name="intellij.python.sdk"/>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<plugin id="org.toml.lang"/>
<module name="intellij.python.community"/>
<module name="intellij.python.community.helpersLocator"/>
<module name="intellij.python.psi"/>
<module name="intellij.python.requirements"/>
<module name="intellij.python.sdk"/>
<module name="intellij.toml.core"/>
<!-- endregion -->
</dependencies>
<extensionPoints>
@@ -3,19 +3,25 @@
package com.jetbrains.python.codeInsight.typing
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.PsiManager
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyUtil
import com.jetbrains.python.psi.resolve.RatedResolveResult
import com.jetbrains.python.psi.resolve.isNamespacePackage
import com.jetbrains.python.pyi.PyiFile
import com.jetbrains.python.pyi.PyiUtil
@@ -137,6 +143,26 @@ private fun getClassOrContentOrSourceRoot(project: Project, file: VirtualFile):
*/
fun isInStubPackage(element: PsiElement) = element.getUserData(STUB_PACKAGE_KEY) == true
internal fun isInPartialStubPackage(element: PsiElement): Boolean {
// The enclosing stub package (the element itself, its package directory, or any ancestor up to the root) is explicitly
// marked as partial via its `py.typed` marker. Works both for package directories and for plain stub modules
// (e.g. `foo-stubs/bar.pyi`), so that submodules of a partial stub package still fall back to the runtime package.
val pyTyped = getPyTyped(element) ?: return false
return isPartialPyTypedMarker(element.project, pyTyped)
}
/**
* Returns `true` if the stub package [element] directly contains a sub-package named [name] that is a namespace package.
*
* Namespace packages cannot ship complete type information (PEP 561), so submodules resolved underneath them must still
* fall back to the runtime package even when the enclosing stub package itself is complete.
*/
internal fun hasNamespaceSubPackage(element: PsiElement, name: String): Boolean {
val packageDir = getPackageDir(element) ?: return false
val subPackage = packageDir.findSubdirectory(name) ?: return false
return isNamespacePackage(subPackage)
}
/**
* See [https://www.python.org/dev/peps/pep-0561/#packaging-type-information].
* Value is cached in element's user data.
@@ -165,12 +191,34 @@ private fun getPyTyped(element: PsiElement?): VirtualFile? {
val root = getClassOrContentOrSourceRoot(element.project, file) ?: return null
var current = if (file.isDirectory) file else file.parent
while (current != null && current != root && current.isDirectory) {
while (current != null && current.isDirectory) {
val pyTyped = current.findChild("py.typed")
if (pyTyped != null && !pyTyped.isDirectory) return pyTyped
if (current == root) break
current = current.parent
}
return null
}
private fun getPackageDir(element: PsiElement): PsiDirectory? {
return when (val packageDir = PyUtil.turnInitIntoDir(element)) {
is PsiDirectory -> packageDir
else -> null
}
}
private fun isPartialPyTypedMarker(project: Project, pyTyped: VirtualFile): Boolean {
val psiFile = PsiManager.getInstance(project).findFile(pyTyped) ?: return false
return CachedValuesManager.getCachedValue(psiFile) {
CachedValueProvider.Result.create(readPyTypedMarker(pyTyped), psiFile)
}
}
private fun readPyTypedMarker(pyTyped: VirtualFile): Boolean {
return runCatching {
val text = FileDocumentManager.getInstance().getCachedDocument(pyTyped)?.text ?: VfsUtilCore.loadText(pyTyped)
"partial\n" in text
}.getOrDefault(false)
}
@@ -16,14 +16,31 @@
package com.jetbrains.python.codeInsight.typing
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.PsiManager
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.QualifiedName
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
import com.jetbrains.python.PythonRuntimeService
import com.jetbrains.python.psi.LanguageLevel
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlFileType
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlLiteral
import org.toml.lang.psi.ext.TomlLiteralKind
import org.toml.lang.psi.ext.kind
import java.io.File
/**
@@ -33,6 +50,10 @@ import java.io.File
*
*/
object PyTypeShed {
private const val METADATA_TOML = "METADATA.toml"
private const val PARTIAL_STUB_KEY = "partial_stub"
private val PARTIAL_THIRD_PARTY_STUB_PACKAGE_KEY =
Key.create<CachedValue<Boolean>>("PyTypeShed.partialThirdPartyStubPackage")
private val stdlibNamesAvailableOnlyInSubsetOfSupportedLanguageLevels = mapOf(
// name to python versions when this name was introduced and removed
@@ -230,12 +251,65 @@ object PyTypeShed {
fun isInStandardLibrary(file: VirtualFile): Boolean =
stdlibRoot?.let { VfsUtilCore.isAncestor(it, file, false) } == true
fun isInPartialThirdPartyStubPackage(element: PsiElement): Boolean {
val file = (element as? PsiFileSystemItem)?.virtualFile ?: element.containingFile?.virtualFile ?: return false
val distributionRoot = getThirdPartyDistributionRoot(file) ?: return false
return isPartialThirdPartyStubPackage(element.project, distributionRoot)
}
private fun getThirdPartyDistributionRoot(file: VirtualFile): VirtualFile? {
val stubsRoot = thirdPartyStubRoot ?: return null
if (!VfsUtilCore.isAncestor(stubsRoot, file, false)) return null
var current = if (file.isDirectory) file else file.parent
while (current != null && current.parent != stubsRoot) {
current = current.parent
}
return current?.takeIf { it.parent == stubsRoot }
}
/**
* Find the directory containing .pyi stubs for the package [packageName] under `typeshed/stubs`.
*
* [packageName] should match the name of the package on PyPI.
*/
fun getStubRootForPackage(packageName: String): VirtualFile? {
return thirdPartyStubRoot?.findChild(packageName)
val stubsRoot = thirdPartyStubRoot ?: return null
return stubsRoot.findChild(packageName)
}
}
private fun isPartialThirdPartyStubPackage(project: Project, distributionRoot: VirtualFile): Boolean {
val psiDirectory = PsiManager.getInstance(project).findDirectory(distributionRoot) ?: return false
val metadataFile = distributionRoot.findChild(METADATA_TOML)
return CachedValuesManager.getManager(project).getCachedValue(psiDirectory, PARTIAL_THIRD_PARTY_STUB_PACKAGE_KEY, {
val metadataPsiFile = metadataFile?.let { PsiManager.getInstance(project).findFile(it) as? TomlFile }
val isPartial = when {
metadataPsiFile != null -> hasPartialStubMarker(metadataPsiFile)
metadataFile != null -> readPartialStubMarker(project, metadataFile)
else -> false
}
CachedValueProvider.Result.create(
isPartial,
metadataPsiFile ?: metadataFile ?: psiDirectory,
VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS,
)
}, false)
}
private fun readPartialStubMarker(project: Project, metadataFile: VirtualFile): Boolean {
val metadataText = FileDocumentManager.getInstance().getCachedDocument(metadataFile)?.text ?: VfsUtilCore.loadText(metadataFile)
val psiFile =
PsiFileFactory.getInstance(project).createFileFromText(METADATA_TOML, TomlFileType, metadataText) as? TomlFile ?: return false
return hasPartialStubMarker(psiFile)
}
private fun hasPartialStubMarker(metadataFile: TomlFile): Boolean {
val partialStubEntry = metadataFile.children
.filterIsInstance<TomlKeyValue>()
.firstOrNull { it.key.text == PARTIAL_STUB_KEY } ?: return false
val literal = partialStubEntry.value as? TomlLiteral ?: return false
return literal.kind is TomlLiteralKind.Boolean && literal.text == "true"
}
}
@@ -0,0 +1,130 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.psi.resolve
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
import com.intellij.util.asDisposable
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
@OptIn(FlowPreview::class)
@Service(Service.Level.PROJECT)
class PyPartialStubMarkersInvalidator(
private val project: Project,
private val coroutineScope: CoroutineScope,
) {
fun subscribe() {
val invalidationRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val parentDisposable = coroutineScope.asDisposable()
coroutineScope.launch {
invalidationRequests
.debounce(INVALIDATION_DELAY)
.collectLatest {
clearPathCaches(project)
}
}
project.messageBus.connect(parentDisposable).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
override fun after(events: MutableList<out VFileEvent>) {
if (events.any { affectsPartialStubMarkers(project, it) }) {
invalidationRequests.tryEmit(Unit)
}
}
})
EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
val file = FileDocumentManager.getInstance().getFile(event.document) ?: return
if (isProjectPartialStubMarkerFile(project, file)) {
invalidationRequests.tryEmit(Unit)
}
}
}, parentDisposable)
}
private fun clearPathCaches(project: Project) {
if (project.isDisposed) return
val sdks = linkedSetOf<Sdk>()
for (module in ModuleManager.getInstance(project).modules) {
PythonModulePathCache.getInstance(module).clearCache()
PythonSdkUtil.findPythonSdk(module)?.let(sdks::add)
}
ProjectRootManager.getInstance(project).projectSdk
?.takeIf(PythonSdkUtil::isPythonSdk)
?.let(sdks::add)
for (sdk in sdks) {
PythonSdkPathCache.getInstance(project, sdk).clearCache()
}
}
private fun isPartialStubMarkerFileName(fileName: String?): Boolean {
return fileName == "py.typed"
}
private fun affectsPartialStubMarkers(project: Project, event: VFileEvent): Boolean {
if (event.file?.let { isProjectPartialStubMarkerFile(project, it) } == true) {
return true
}
return when (event) {
is VFileCopyEvent ->
isPartialStubMarkerFileName(event.newChildName) &&
isProjectRelevantLocation(project, event.newParent)
is VFilePropertyChangeEvent ->
event.isRename &&
(isPartialStubMarkerFileName(event.oldValue as? String) || isPartialStubMarkerFileName(event.newValue as? String)) &&
event.file.parent?.let { isProjectRelevantLocation(project, it) } == true
else -> false
}
}
private fun isProjectPartialStubMarkerFile(project: Project, file: VirtualFile): Boolean {
return isPartialStubMarkerFileName(file.name) && isProjectRelevantLocation(project, file)
}
private fun isProjectRelevantLocation(project: Project, file: VirtualFile): Boolean {
if (!project.isInitialized || !file.isValid) {
return false
}
val index = ProjectFileIndex.getInstance(project)
return index.isInProject(file)
}
companion object {
private val INVALIDATION_DELAY = 300.milliseconds
}
}
internal class PyPartialStubMarkersInvalidatorActivity : ProjectActivity {
override suspend fun execute(project: Project) {
project.service<PyPartialStubMarkersInvalidator>().subscribe()
}
}
@@ -23,7 +23,9 @@ import com.intellij.psi.util.QualifiedName
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.jetbrains.python.codeInsight.typing.PyTypeShed
import com.jetbrains.python.codeInsight.typing.hasNamespaceSubPackage
import com.jetbrains.python.codeInsight.typing.isInInlinePackage
import com.jetbrains.python.codeInsight.typing.isInPartialStubPackage
import com.jetbrains.python.codeInsight.typing.isInStubPackage
import com.jetbrains.python.facet.PythonPathContributingFacet
import com.jetbrains.python.module.PyModuleService
@@ -113,7 +115,7 @@ private fun resolveModuleFromRoots(name: QualifiedName, context: PyQualifiedName
val head = name.removeTail(name.componentCount - 1)
val nameNoHead = name.removeHead(1)
return nameNoHead.components.fold(resultsFromRoots(head, context).distinct()) { results, component ->
filterTopPriorityResults(results, context.module)
filterTopPriorityResults(results, context.module, component)
.asSequence()
.filterIsInstance<PsiFileSystemItem>()
.flatMap { resolveModuleAt(QualifiedName.fromComponents(component), it, context).asSequence() }
@@ -408,7 +410,7 @@ private fun filterTopPriorityResultsWithFallback(
* Filters resolved elements according to their import priority in sys.path and
* [PEP 561](https://www.python.org/dev/peps/pep-0561/#type-checker-module-resolution-order) rules.
*/
private fun filterTopPriorityResults(resolved: List<PsiElement>, module: Module?): List<PsiElement> {
private fun filterTopPriorityResults(resolved: List<PsiElement>, module: Module?, nextComponent: String? = null): List<PsiElement> {
if (resolved.isEmpty()) return emptyList()
val groupedResults = resolved.groupByTo(sortedMapOf<Priority, MutableList<PsiElement>>()) { resolvedElementPriority(it, module) }
@@ -417,12 +419,12 @@ private fun filterTopPriorityResults(resolved: List<PsiElement>, module: Module?
if (groupedResults.topResultIs(Priority.NAMESPACE_PACKAGE)) return groupedResults[Priority.NAMESPACE_PACKAGE]!! + skeletons
groupedResults.remove(Priority.NAMESPACE_PACKAGE)
val topPriority = groupedResults.keys.firstOrNull()
val topResult = topPriority?.let { groupedResults[it]?.firstOrNull() }
val priorityResults = when {
groupedResults.isEmpty() -> emptyList()
// stub packages can be partial
groupedResults.topResultIs(Priority.STUB_PACKAGE) -> firstResultWithFallback(groupedResults, Priority.STUB_PACKAGE)
// third party sdk should not overwrite packages from the same vendor
groupedResults.topResultIs(Priority.THIRD_PARTY_SDK) -> firstResultWithFallback(groupedResults, Priority.THIRD_PARTY_SDK)
topPriority != null && topResult != null && shouldFallbackToNextPriority(topPriority, topResult, module, nextComponent) ->
firstResultWithFallback(groupedResults, topPriority)
else -> listOf(groupedResults.values.first().first())
}
return priorityResults + skeletons
@@ -439,6 +441,25 @@ private fun firstResultWithFallback(results: SortedMap<Priority, MutableList<Psi
return listOfNotNull(first, nextByPriority)
}
private fun shouldFallbackToNextPriority(priority: Priority, element: PsiElement, module: Module?, nextComponent: String?): Boolean {
return when (priority) {
Priority.USER_STUB -> isUserFile(element, module) && isInPartialStub(element, nextComponent)
Priority.STUB_PACKAGE -> isInPartialStub(element, nextComponent)
Priority.TYPESHED -> PyTypeShed.isInPartialThirdPartyStubPackage(element)
Priority.THIRD_PARTY_SDK -> true
else -> false
}
}
/**
* A stub package result must keep the runtime package as a fallback when the stub does not authoritatively cover the
* path being resolved: either the enclosing stub package is marked partial, or the next component to resolve is a
* namespace package inside the stub (namespace packages are inherently incomplete, see PEP 561).
*/
private fun isInPartialStub(element: PsiElement, nextComponent: String?): Boolean {
return isInPartialStubPackage(element) || (nextComponent != null && hasNamespaceSubPackage(element, nextComponent))
}
/**
* See [https://www.python.org/dev/peps/pep-0561/#type-checker-module-resolution-order].
*/
@@ -491,5 +512,3 @@ private enum class Priority {
NAMESPACE_PACKAGE, // namespace package but may contain several entries in resolve result
SKELETON // generated skeletons have lowest priority but are always included in the resolve result as a fallback
}
@@ -0,0 +1,3 @@
from mypackage.module2 import d
print(d)
@@ -0,0 +1,3 @@
from mypackage.module2 import d
print(d)
@@ -0,0 +1,3 @@
from mypackage.module2 import d
print(d)
@@ -0,0 +1 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@@ -0,0 +1 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@@ -0,0 +1,4 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
def bar(a: str, b: str):
return a + b
@@ -0,0 +1,6 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
from pkg import foo
foo.bar("a", "b")
# <ref>
@@ -0,0 +1 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@@ -0,0 +1,4 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
def foo():
return 42
@@ -0,0 +1,6 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
from synthetic_complete_typeshed import shortcuts
shortcuts.foo()
# <ref>
@@ -0,0 +1 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@@ -0,0 +1,4 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
def bar(a, b):
return a + b
@@ -0,0 +1,6 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
from pkg import foo
foo.bar("a", "b")
# <ref>
@@ -0,0 +1 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@@ -0,0 +1,4 @@
from synthetic_false_partial_typeshed import shortcuts
shortcuts.foo()
# <ref>
@@ -0,0 +1 @@
not-partial-no-no
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,4 @@
from pkg import foo
foo.bar("a", "b")
# <ref>
@@ -0,0 +1 @@
def declared(a: str, b: str) -> str: ...
@@ -0,0 +1,6 @@
def declared(a, b):
return a + b
def runtime_only(a, b):
return a + b
@@ -0,0 +1,4 @@
from pkg import foo
foo.runtime_only("a", "b")
# <ref>
@@ -0,0 +1 @@
def stub_only() -> None: ...
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,4 @@
from pkg.nspkg import foo
foo.bar("a", "b")
# <ref>
@@ -0,0 +1,4 @@
from mypackage.package2 import mymodule
mymodule.bar("a", "b")
# <ref>
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,4 @@
from pkg.foo import bar
bar("a", "b")
# <ref>
@@ -0,0 +1,2 @@
def foo():
return 42
@@ -0,0 +1,4 @@
from synthetic_partial_typeshed import shortcuts
shortcuts.foo()
# <ref>
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,4 @@
from pkg import foo
foo.bar("a", "b")
# <ref>
@@ -0,0 +1,4 @@
from mypackage import mymodule
mymodule.bar("a", "b")
# <ref>
@@ -0,0 +1,2 @@
def bar(a, b):
return a + b
@@ -0,0 +1,2 @@
version = "1.0.*"
upstream_repository = "https://example.invalid/pycharm-test-complete-typeshed"
@@ -0,0 +1,3 @@
version = "1.0.*"
upstream_repository = "https://example.invalid/pycharm-test-false-partial-typeshed"
partial_stub = false
@@ -0,0 +1,3 @@
version = "1.0.*"
upstream_repository = "https://example.invalid/pycharm-test-partial-typeshed"
partial_stub = true
@@ -1,14 +1,22 @@
// Copyright 2000-2021 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;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.RecursionManager;
import com.intellij.openapi.util.StackOverflowPreventedException;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiPolyVariantReference;
import com.intellij.psi.PsiReference;
import com.intellij.psi.ResolveResult;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.PsiTestUtil;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.fixtures.PyResolveTestCase;
import com.jetbrains.python.fixtures.PyTestCase;
@@ -24,6 +32,8 @@ import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.pyi.PyiUtil;
import java.io.IOException;
public class Py3ResolveTest extends PyResolveTestCase {
@@ -732,9 +742,23 @@ public class Py3ResolveTest extends PyResolveTestCase {
);
}
// TODO: this should be fixed after introducing an ability to check visited paths while resolving some qualified name
// PY-30942
public void _testNoInlinePackageInsteadStubPackage() {
// PY-55589
public void testPyInsteadPartialStubPackageAnotherImport() {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> {
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(element, PyFunction.class);
assertEquals("foo.py", element.getContainingFile().getName());
}
);
}
// PY-55589
public void testNoRuntimeMembersInsteadPartialStubPackageModule() {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
@@ -744,9 +768,8 @@ public class Py3ResolveTest extends PyResolveTestCase {
);
}
// TODO: this should be fixed after introducing an ability to check visited paths while resolving some qualified name
// PY-30942
public void _testNoInlinePackageInsteadStubPackageAnotherImport() {
public void testNoInlinePackageInsteadStubPackage() {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
@@ -756,6 +779,212 @@ public class Py3ResolveTest extends PyResolveTestCase {
);
}
// PY-30942
public void testNoInlinePackageInsteadStubPackageAnotherImport() {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
);
}
// PY-55589
public void testNoPyInsteadNonPartialStubPackageMarker() {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
);
}
// PY-55589
public void testPyInsteadNamespaceStubPackage() {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> {
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(element, PyFunction.class);
assertEquals("foo.py", element.getContainingFile().getName());
}
);
}
// PY-55589
public void testPyInsteadPartialUserStubPackage() {
final String path = "resolve/" + getTestName(false);
myFixture.copyDirectoryToProject(path + "/project", "");
myFixture.configureByFile("main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> {
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(element, PyFunction.class);
assertEquals("foo.py", element.getContainingFile().getName());
}
);
}
// PY-55589
public void testPyInsteadPartialUserStubPackageSourceRoot() {
final String path = "resolve/" + getTestName(false);
myFixture.copyDirectoryToProject(path, "");
myFixture.configureByFile("main.py");
final Module module = myFixture.getModule();
final VirtualFile sourceRoot = myFixture.findFileInTempDir("stubs");
assertNotNull(sourceRoot);
PsiTestUtil.addSourceRoot(module, sourceRoot);
try {
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(element, PyFunction.class);
assertEquals("mymodule.py", element.getContainingFile().getName());
}
finally {
PsiTestUtil.removeSourceRoot(module, sourceRoot);
}
}
// PY-55589
public void testPyInsteadNestedPartialUserStubPackage() {
final String path = "resolve/" + getTestName(false);
myFixture.copyDirectoryToProject(path, "");
myFixture.configureByFile("main.py");
final Module module = myFixture.getModule();
final VirtualFile sourceRoot = myFixture.findFileInTempDir("stubs");
assertNotNull(sourceRoot);
PsiTestUtil.addSourceRoot(module, sourceRoot);
try {
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(element, PyFunction.class);
assertEquals("mymodule.py", element.getContainingFile().getName());
}
finally {
PsiTestUtil.removeSourceRoot(module, sourceRoot);
}
}
// PY-55589
public void testEditingPyTypedInvalidatesPartialUserStubPackageResolveCache() {
final String path = "resolve/PyInsteadPartialUserStubPackage";
myFixture.copyDirectoryToProject(path + "/project", "");
myFixture.configureByFile("main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> {
final PsiElement initial = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(initial, PyFunction.class);
assertEquals("foo.py", initial.getContainingFile().getName());
final VirtualFile pyTyped = myFixture.findFileInTempDir("pkg/py.typed");
assertNotNull(pyTyped);
WriteAction.run(() -> {
try {
VfsUtil.saveText(pyTyped, "");
}
catch (IOException e) {
throw new RuntimeException(e);
}
});
PlatformTestUtil.waitForAlarm(300);
assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve());
}
);
}
// PY-55589
public void testEditingUnsavedPyTypedInvalidatesPartialUserStubPackageResolveCache() {
final String path = "resolve/PyInsteadPartialUserStubPackage";
myFixture.copyDirectoryToProject(path + "/project", "");
myFixture.configureByFile("main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> {
final PsiElement initial = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(initial, PyFunction.class);
assertEquals("foo.py", initial.getContainingFile().getName());
final VirtualFile pyTyped = myFixture.findFileInTempDir("pkg/py.typed");
assertNotNull(pyTyped);
final Document pyTypedDocument = FileDocumentManager.getInstance().getDocument(pyTyped);
assertNotNull(pyTypedDocument);
WriteAction.run(() -> pyTypedDocument.setText(""));
assertTrue(FileDocumentManager.getInstance().isDocumentUnsaved(pyTypedDocument));
PlatformTestUtil.waitForAlarm(300);
assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve());
}
);
}
// PY-55589
public void testNoPyInsteadCompleteUserStubPackage() {
final String path = "resolve/" + getTestName(false);
myFixture.copyDirectoryToProject(path + "/project", "");
myFixture.configureByFile("main.py");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
);
}
// PY-65967
public void testPyInsteadPartialTypeShed() throws IOException {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
enableTestDataTypeshedStubsForPackages("pycharm-test-partial-typeshed");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> {
final PsiElement element = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertInstanceOf(element, PyFunction.class);
assertEquals("shortcuts.py", element.getContainingFile().getName());
}
);
}
// PY-65967
public void testNoPyInsteadCompleteTypeShed() throws IOException {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
enableTestDataTypeshedStubsForPackages("pycharm-test-complete-typeshed");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
);
}
// PY-65967
public void testNoPyInsteadFalsePartialTypeShed() throws IOException {
final String path = "resolve/" + getTestName(false);
myFixture.configureByFile(path + "/main.py");
enableTestDataTypeshedStubsForPackages("pycharm-test-false-partial-typeshed");
runWithAdditionalClassEntryInSdkRoots(
path + "/lib",
() -> assertNull(PyResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve())
);
}
// PY-31354
public void testStubPackageInOtherRoot() {
final String path = "resolve/" + getTestName(false);

Some files were not shown because too many files have changed in this diff Show More