diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index fe4075ee3091..174dbbf1becc 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -160,6 +160,7 @@
+
diff --git a/python/python-psi-impl/BUILD.bazel b/python/python-psi-impl/BUILD.bazel
index f2f2dfa1c7bf..2b23fe039cd6 100644
--- a/python/python-psi-impl/BUILD.bazel
+++ b/python/python-psi-impl/BUILD.bazel
@@ -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
diff --git a/python/python-psi-impl/intellij.python.psi.impl.iml b/python/python-psi-impl/intellij.python.psi.impl.iml
index 0d5e3e9217d9..a94700d1a92c 100644
--- a/python/python-psi-impl/intellij.python.psi.impl.iml
+++ b/python/python-psi-impl/intellij.python.psi.impl.iml
@@ -52,5 +52,7 @@
+
+
\ No newline at end of file
diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
index e74a1e7d03c5..3e8540ce8148 100644
--- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml
+++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
@@ -1,10 +1,14 @@
-
-
+
+
+
+
+
+
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyStubPackages.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyStubPackages.kt
index d6bf5af6a73a..d1d7ee3f5b83 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyStubPackages.kt
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyStubPackages.kt
@@ -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)
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypeShed.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypeShed.kt
index 7a4d50e3c652..9959da979f40 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypeShed.kt
+++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypeShed.kt
@@ -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>("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)
}
-}
\ No newline at end of file
+
+ 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()
+ .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"
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyPartialStubMarkersInvalidator.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyPartialStubMarkersInvalidator.kt
new file mode 100644
index 000000000000..72f6d5a46b7f
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyPartialStubMarkersInvalidator.kt
@@ -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(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) {
+ 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()
+
+ 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().subscribe()
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt
index 8b8b75dd59ec..0b5bd981875f 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt
+++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt
@@ -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()
.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, module: Module?): List {
+private fun filterTopPriorityResults(resolved: List, module: Module?, nextComponent: String? = null): List {
if (resolved.isEmpty()) return emptyList()
val groupedResults = resolved.groupByTo(sortedMapOf>()) { resolvedElementPriority(it, module) }
@@ -417,12 +419,12 @@ private fun filterTopPriorityResults(resolved: List, 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 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
}
-
-
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/__init__.py
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/__init__.py
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/a.py
new file mode 100644
index 000000000000..4034126c4b4d
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/a.py
@@ -0,0 +1,3 @@
+from mypackage.module2 import d
+
+print(d)
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/__init__.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/__init__.pyi
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/__init__.pyi
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/mymodule.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/mymodule.pyi
new file mode 100644
index 000000000000..efd1039da217
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/mymodule.pyi
@@ -0,0 +1 @@
+b: int
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/py.typed b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage-stubs/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/__init__.py
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/__init__.py
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/module2.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/module2.py
new file mode 100644
index 000000000000..6e6d28cebd12
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/module2.py
@@ -0,0 +1 @@
+d = ["hello"]
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/mymodule.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/mymodule.py
new file mode 100644
index 000000000000..8f1cdfd5d44b
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUser/mypackage/mymodule.py
@@ -0,0 +1,2 @@
+b = 1
+c = ""
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/__init__.py
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/__init__.py
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/a/b.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/a/b.py
new file mode 100644
index 000000000000..4034126c4b4d
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/a/b.py
@@ -0,0 +1,3 @@
+from mypackage.module2 import d
+
+print(d)
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/__init__.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/__init__.pyi
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/__init__.pyi
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/mymodule.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/mymodule.pyi
new file mode 100644
index 000000000000..efd1039da217
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/mymodule.pyi
@@ -0,0 +1 @@
+b: int
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/py.typed b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage-stubs/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/__init__.py
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/__init__.py
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/module2.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/module2.py
new file mode 100644
index 000000000000..6e6d28cebd12
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/module2.py
@@ -0,0 +1 @@
+d = ["hello"]
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/mymodule.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/mymodule.py
new file mode 100644
index 000000000000..8f1cdfd5d44b
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNested/mypackage/mymodule.py
@@ -0,0 +1,2 @@
+b = 1
+c = ""
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/__init__.py
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/__init__.py
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/a.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/a.pyi
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/a.pyi
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/a/b.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/a/b.py
new file mode 100644
index 000000000000..4034126c4b4d
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/a/b.py
@@ -0,0 +1,3 @@
+from mypackage.module2 import d
+
+print(d)
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/__init__.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/__init__.pyi
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/__init__.pyi
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/mymodule.pyi b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/mymodule.pyi
new file mode 100644
index 000000000000..efd1039da217
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/mymodule.pyi
@@ -0,0 +1 @@
+b: int
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/py.typed b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage-stubs/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/__init__.py
new file mode 100644
index 000000000000..8b137891791f
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/__init__.py
@@ -0,0 +1 @@
+
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/module2.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/module2.py
new file mode 100644
index 000000000000..6e6d28cebd12
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/module2.py
@@ -0,0 +1 @@
+d = ["hello"]
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/mymodule.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/mymodule.py
new file mode 100644
index 000000000000..8f1cdfd5d44b
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PartialStubPackageUserNestedWithSiblingStub/mypackage/mymodule.py
@@ -0,0 +1,2 @@
+b = 1
+c = ""
diff --git a/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg-stubs/__init__.pyi b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg-stubs/__init__.pyi
new file mode 100644
index 000000000000..ea4c9c8b4e1a
--- /dev/null
+++ b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg-stubs/__init__.pyi
@@ -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.
diff --git a/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/__init__.py b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/__init__.py
new file mode 100644
index 000000000000..ea4c9c8b4e1a
--- /dev/null
+++ b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/__init__.py
@@ -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.
diff --git a/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/foo.py b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/foo.py
new file mode 100644
index 000000000000..340ba28864f0
--- /dev/null
+++ b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/foo.py
@@ -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
diff --git a/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/py.typed b/python/testData/resolve/InlinePackageInsteadStubPackage/lib/pkg/py.typed
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/InlinePackageInsteadStubPackage/main.py b/python/testData/resolve/InlinePackageInsteadStubPackage/main.py
new file mode 100644
index 000000000000..9cea504d3521
--- /dev/null
+++ b/python/testData/resolve/InlinePackageInsteadStubPackage/main.py
@@ -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")
+# [
diff --git a/python/testData/resolve/NoPyInsteadCompleteTypeShed/lib/synthetic_complete_typeshed/__init__.py b/python/testData/resolve/NoPyInsteadCompleteTypeShed/lib/synthetic_complete_typeshed/__init__.py
new file mode 100644
index 000000000000..ea4c9c8b4e1a
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteTypeShed/lib/synthetic_complete_typeshed/__init__.py
@@ -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.
diff --git a/python/testData/resolve/NoPyInsteadCompleteTypeShed/lib/synthetic_complete_typeshed/shortcuts.py b/python/testData/resolve/NoPyInsteadCompleteTypeShed/lib/synthetic_complete_typeshed/shortcuts.py
new file mode 100644
index 000000000000..40b25115f6d6
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteTypeShed/lib/synthetic_complete_typeshed/shortcuts.py
@@ -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
diff --git a/python/testData/resolve/NoPyInsteadCompleteTypeShed/main.py b/python/testData/resolve/NoPyInsteadCompleteTypeShed/main.py
new file mode 100644
index 000000000000..7d18fd483e48
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteTypeShed/main.py
@@ -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()
+# ][
diff --git a/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/lib/pkg/__init__.py b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/lib/pkg/__init__.py
new file mode 100644
index 000000000000..ea4c9c8b4e1a
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/lib/pkg/__init__.py
@@ -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.
diff --git a/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/lib/pkg/foo.py b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/lib/pkg/foo.py
new file mode 100644
index 000000000000..bd56fbf3357f
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/lib/pkg/foo.py
@@ -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
diff --git a/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/project/main.py b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/project/main.py
new file mode 100644
index 000000000000..d727a5f6e6e3
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/project/main.py
@@ -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")
+# ][
diff --git a/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/project/pkg/__init__.pyi b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/project/pkg/__init__.pyi
new file mode 100644
index 000000000000..ea4c9c8b4e1a
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadCompleteUserStubPackage/project/pkg/__init__.pyi
@@ -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.
diff --git a/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/lib/synthetic_false_partial_typeshed/__init__.py b/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/lib/synthetic_false_partial_typeshed/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/lib/synthetic_false_partial_typeshed/shortcuts.py b/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/lib/synthetic_false_partial_typeshed/shortcuts.py
new file mode 100644
index 000000000000..9e67bdd291cb
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/lib/synthetic_false_partial_typeshed/shortcuts.py
@@ -0,0 +1,2 @@
+def foo():
+ return 42
diff --git a/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/main.py b/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/main.py
new file mode 100644
index 000000000000..7ab583da2837
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadFalsePartialTypeShed/main.py
@@ -0,0 +1,4 @@
+from synthetic_false_partial_typeshed import shortcuts
+
+shortcuts.foo()
+# ][
diff --git a/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg-stubs/__init__.pyi b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg-stubs/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg-stubs/py.typed b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg-stubs/py.typed
new file mode 100644
index 000000000000..a44b4700a63a
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg-stubs/py.typed
@@ -0,0 +1 @@
+not-partial-no-no
diff --git a/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg/__init__.py b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg/foo.py b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg/foo.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/lib/pkg/foo.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/main.py b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/main.py
new file mode 100644
index 000000000000..cc84d4e5f2b2
--- /dev/null
+++ b/python/testData/resolve/NoPyInsteadNonPartialStubPackageMarker/main.py
@@ -0,0 +1,4 @@
+from pkg import foo
+
+foo.bar("a", "b")
+# ][
diff --git a/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/__init__.pyi b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/foo.pyi b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/foo.pyi
new file mode 100644
index 000000000000..3532afeb86e2
--- /dev/null
+++ b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/foo.pyi
@@ -0,0 +1 @@
+def declared(a: str, b: str) -> str: ...
diff --git a/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/py.typed b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg-stubs/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg/__init__.py b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg/foo.py b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg/foo.py
new file mode 100644
index 000000000000..b48b06f19775
--- /dev/null
+++ b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/lib/pkg/foo.py
@@ -0,0 +1,6 @@
+def declared(a, b):
+ return a + b
+
+
+def runtime_only(a, b):
+ return a + b
diff --git a/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/main.py b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/main.py
new file mode 100644
index 000000000000..90ff9fa2a102
--- /dev/null
+++ b/python/testData/resolve/NoRuntimeMembersInsteadPartialStubPackageModule/main.py
@@ -0,0 +1,4 @@
+from pkg import foo
+
+foo.runtime_only("a", "b")
+# ][
diff --git a/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg-stubs/__init__.pyi b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg-stubs/__init__.pyi
new file mode 100644
index 000000000000..2ae28399f5fd
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg-stubs/__init__.pyi
@@ -0,0 +1 @@
+pass
diff --git a/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg-stubs/nspkg/bar.pyi b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg-stubs/nspkg/bar.pyi
new file mode 100644
index 000000000000..7a7dbb4eb832
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg-stubs/nspkg/bar.pyi
@@ -0,0 +1 @@
+def stub_only() -> None: ...
diff --git a/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg/__init__.py b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg/__init__.py
new file mode 100644
index 000000000000..2ae28399f5fd
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg/__init__.py
@@ -0,0 +1 @@
+pass
diff --git a/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg/nspkg/foo.py b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg/nspkg/foo.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNamespaceStubPackage/lib/pkg/nspkg/foo.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/PyInsteadNamespaceStubPackage/main.py b/python/testData/resolve/PyInsteadNamespaceStubPackage/main.py
new file mode 100644
index 000000000000..9b62fdc5ce86
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNamespaceStubPackage/main.py
@@ -0,0 +1,4 @@
+from pkg.nspkg import foo
+
+foo.bar("a", "b")
+# ][
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/main.py b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/main.py
new file mode 100644
index 000000000000..de6846df228a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/main.py
@@ -0,0 +1,4 @@
+from mypackage.package2 import mymodule
+
+mymodule.bar("a", "b")
+# ][
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/__init__.py b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/mymodule.py b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/mymodule.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/mymodule.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/package2/__init__.py b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/package2/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/package2/mymodule.py b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/package2/mymodule.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/mypackage/package2/mymodule.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/__init__.pyi b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/package2/__init__.pyi b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/package2/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/package2/py.typed b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/package2/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/package2/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/py.typed b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/py.typed
new file mode 100644
index 000000000000..7ecb56eb3fa3
--- /dev/null
+++ b/python/testData/resolve/PyInsteadNestedPartialUserStubPackage/stubs/mypackage/py.typed
@@ -0,0 +1 @@
+no
diff --git a/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg-stubs/__init__.pyi b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg-stubs/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg-stubs/py.typed b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg-stubs/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg-stubs/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg/__init__.py b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg/foo.py b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg/foo.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/lib/pkg/foo.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/main.py b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/main.py
new file mode 100644
index 000000000000..3642a7adb99d
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialStubPackageAnotherImport/main.py
@@ -0,0 +1,4 @@
+from pkg.foo import bar
+
+bar("a", "b")
+# ][
diff --git a/python/testData/resolve/PyInsteadPartialTypeShed/lib/synthetic_partial_typeshed/__init__.py b/python/testData/resolve/PyInsteadPartialTypeShed/lib/synthetic_partial_typeshed/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialTypeShed/lib/synthetic_partial_typeshed/shortcuts.py b/python/testData/resolve/PyInsteadPartialTypeShed/lib/synthetic_partial_typeshed/shortcuts.py
new file mode 100644
index 000000000000..9e67bdd291cb
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialTypeShed/lib/synthetic_partial_typeshed/shortcuts.py
@@ -0,0 +1,2 @@
+def foo():
+ return 42
diff --git a/python/testData/resolve/PyInsteadPartialTypeShed/main.py b/python/testData/resolve/PyInsteadPartialTypeShed/main.py
new file mode 100644
index 000000000000..917ce7a5d1b9
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialTypeShed/main.py
@@ -0,0 +1,4 @@
+from synthetic_partial_typeshed import shortcuts
+
+shortcuts.foo()
+# ][
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackage/lib/pkg/__init__.py b/python/testData/resolve/PyInsteadPartialUserStubPackage/lib/pkg/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackage/lib/pkg/foo.py b/python/testData/resolve/PyInsteadPartialUserStubPackage/lib/pkg/foo.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialUserStubPackage/lib/pkg/foo.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackage/project/main.py b/python/testData/resolve/PyInsteadPartialUserStubPackage/project/main.py
new file mode 100644
index 000000000000..8f6cba0985a8
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialUserStubPackage/project/main.py
@@ -0,0 +1,4 @@
+from pkg import foo
+
+foo.bar("a", "b")
+# ][
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackage/project/pkg/__init__.pyi b/python/testData/resolve/PyInsteadPartialUserStubPackage/project/pkg/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackage/project/pkg/py.typed b/python/testData/resolve/PyInsteadPartialUserStubPackage/project/pkg/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialUserStubPackage/project/pkg/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/main.py b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/main.py
new file mode 100644
index 000000000000..01631fb5872b
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/main.py
@@ -0,0 +1,4 @@
+from mypackage import mymodule
+
+mymodule.bar("a", "b")
+# ][
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/mypackage/__init__.py b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/mypackage/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/mypackage/mymodule.py b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/mypackage/mymodule.py
new file mode 100644
index 000000000000..c3fbc1014d0a
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/mypackage/mymodule.py
@@ -0,0 +1,2 @@
+def bar(a, b):
+ return a + b
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/stubs/mypackage/__init__.pyi b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/stubs/mypackage/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/stubs/mypackage/py.typed b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/stubs/mypackage/py.typed
new file mode 100644
index 000000000000..b648ac923333
--- /dev/null
+++ b/python/testData/resolve/PyInsteadPartialUserStubPackageSourceRoot/stubs/mypackage/py.typed
@@ -0,0 +1 @@
+partial
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/METADATA.toml b/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/METADATA.toml
new file mode 100644
index 000000000000..48508735a58e
--- /dev/null
+++ b/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/METADATA.toml
@@ -0,0 +1,2 @@
+version = "1.0.*"
+upstream_repository = "https://example.invalid/pycharm-test-complete-typeshed"
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/synthetic_complete_typeshed/__init__.pyi b/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/synthetic_complete_typeshed/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/synthetic_complete_typeshed/existing_module.pyi b/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/synthetic_complete_typeshed/existing_module.pyi
new file mode 100644
index 000000000000..89aab3f5245d
--- /dev/null
+++ b/python/testData/resolve/typeshed/stubs/pycharm-test-complete-typeshed/synthetic_complete_typeshed/existing_module.pyi
@@ -0,0 +1 @@
+def existing() -> int: ...
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-false-partial-typeshed/METADATA.toml b/python/testData/resolve/typeshed/stubs/pycharm-test-false-partial-typeshed/METADATA.toml
new file mode 100644
index 000000000000..437472b815a7
--- /dev/null
+++ b/python/testData/resolve/typeshed/stubs/pycharm-test-false-partial-typeshed/METADATA.toml
@@ -0,0 +1,3 @@
+version = "1.0.*"
+upstream_repository = "https://example.invalid/pycharm-test-false-partial-typeshed"
+partial_stub = false
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-false-partial-typeshed/synthetic_false_partial_typeshed/__init__.pyi b/python/testData/resolve/typeshed/stubs/pycharm-test-false-partial-typeshed/synthetic_false_partial_typeshed/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/METADATA.toml b/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/METADATA.toml
new file mode 100644
index 000000000000..72344e01a22b
--- /dev/null
+++ b/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/METADATA.toml
@@ -0,0 +1,3 @@
+version = "1.0.*"
+upstream_repository = "https://example.invalid/pycharm-test-partial-typeshed"
+partial_stub = true
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/synthetic_partial_typeshed/__init__.pyi b/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/synthetic_partial_typeshed/__init__.pyi
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/synthetic_partial_typeshed/existing_module.pyi b/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/synthetic_partial_typeshed/existing_module.pyi
new file mode 100644
index 000000000000..89aab3f5245d
--- /dev/null
+++ b/python/testData/resolve/typeshed/stubs/pycharm-test-partial-typeshed/synthetic_partial_typeshed/existing_module.pyi
@@ -0,0 +1 @@
+def existing() -> int: ...
diff --git a/python/testSrc/com/jetbrains/python/Py3ResolveTest.java b/python/testSrc/com/jetbrains/python/Py3ResolveTest.java
index 225d3c9a10ae..2d4104b73fac 100644
--- a/python/testSrc/com/jetbrains/python/Py3ResolveTest.java
+++ b/python/testSrc/com/jetbrains/python/Py3ResolveTest.java
@@ -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);
diff --git a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java
index b34784d0b61a..fce5f869ec22 100644
--- a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java
+++ b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java
@@ -85,6 +85,7 @@ import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -311,6 +312,35 @@ public abstract class PyTestCase extends UsefulTestCase {
IndexingTestUtil.waitUntilIndexesAreReadyInAllOpenedProjects();
}
+ protected void enableTestDataTypeshedStubsForPackages(String @NotNull ... packageNames) throws IOException {
+ final String absPath = getTestDataPath() + "/resolve/typeshed/stubs";
+ final VirtualFile sourceThirdPartyStubRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(absPath);
+ assertNotNull("Third-party typeshed root '" + absPath + "' not found", sourceThirdPartyStubRoot);
+
+ final VirtualFile targetThirdPartyStubRoot = PyTypeShed.INSTANCE.getThirdPartyStubRoot();
+ assertNotNull("Bundled third-party typeshed root not found", targetThirdPartyStubRoot);
+
+ final List copiedStubRoots = new ArrayList<>();
+ WriteAction.run(() -> {
+ for (String packageName : packageNames) {
+ final VirtualFile sourceStubRoot = sourceThirdPartyStubRoot.findChild(packageName);
+ assertNotNull("Stub package root for " + packageName + " not found under " + absPath, sourceStubRoot);
+
+ final VirtualFile existingStubRoot = targetThirdPartyStubRoot.findChild(packageName);
+ if (existingStubRoot != null) {
+ VfsTestUtil.deleteFile(existingStubRoot);
+ }
+
+ final VirtualFile targetStubRoot = VfsTestUtil.createDir(targetThirdPartyStubRoot, packageName);
+ VfsUtil.copyDirectory(this, sourceStubRoot, targetStubRoot, null);
+ copiedStubRoots.add(targetStubRoot);
+ }
+ });
+
+ Disposer.register(getTestRootDisposable(), () -> WriteAction.run(() -> copiedStubRoots.forEach(VfsTestUtil::deleteFile)));
+ enablePyiStubsForPackages(packageNames);
+ }
+
protected void enablePyiStubsForPackages(String @NotNull ... packageNames) {
Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
assertNotNull(sdk);
diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java
index 67b8c6ff797b..f62152674fc8 100644
--- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java
+++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java
@@ -37,7 +37,11 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase
}
protected void doMultiFileTest(@NotNull String filename, @NotNull List sourceRoots) {
- myFixture.copyDirectoryToProject(getTestDirectoryPath(), "");
+ doMultiFileTest(getTestName(false), filename, sourceRoots);
+ }
+
+ protected void doMultiFileTest(@NotNull String testDirectory, @NotNull String filename, @NotNull List sourceRoots) {
+ myFixture.copyDirectoryToProject(TEST_DIRECTORY + testDirectory, "");
final Module module = myFixture.getModule();
for (String root : sourceRoots) {
PsiTestUtil.addSourceRoot(module, myFixture.findFileInTempDir(root));
@@ -511,6 +515,26 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase
});
}
+ // PY-55589
+ public void testPartialStubPackageUser() {
+ doMultiFileTest("a.py");
+ }
+
+ // PY-55589
+ public void testPartialStubPackageUserNested() {
+ doMultiFileTest("a/b.py");
+ }
+
+ // PY-55589
+ public void testPartialStubPackageUserNestedWithSiblingStub() {
+ doMultiFileTest("a/b.py");
+ }
+
+ // PY-55589
+ public void testPartialStubPackageUserNestedWithSiblingStubSourceRoot() {
+ doMultiFileTest("PartialStubPackageUserNestedWithSiblingStub", "a/b.py", Collections.singletonList("mypackage-stubs"));
+ }
+
// PY-85880
public void testLiteralUnionInTuple() {
doTestByText("""
]