diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index 47e66d75db5a..6232b90b18a2 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -77,6 +77,7 @@ import com.jetbrains.python.refactoring.classes.PyDependenciesComparator; import com.jetbrains.python.refactoring.classes.extractSuperclass.PyExtractSuperclassHelper; import com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo; import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -992,8 +993,9 @@ public class PyUtil { } // don't touch non-dirs } + @Contract("null -> null; !null -> !null") @Nullable - public static PsiElement turnInitIntoDir(PsiElement target) { + public static PsiElement turnInitIntoDir(@Nullable PsiElement target) { if (target instanceof PyFile && isPackage((PsiFile)target)) { return ((PsiFile)target).getContainingDirectory(); } diff --git a/python/src/com/jetbrains/python/psi/types/PyImportedModuleType.java b/python/src/com/jetbrains/python/psi/types/PyImportedModuleType.java index bc26a1a87ba2..01bcf032bfa2 100644 --- a/python/src/com/jetbrains/python/psi/types/PyImportedModuleType.java +++ b/python/src/com/jetbrains/python/psi/types/PyImportedModuleType.java @@ -15,17 +15,20 @@ */ package com.jetbrains.python.psi.types; +import com.google.common.collect.Sets; import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.util.Condition; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; -import com.intellij.psi.util.QualifiedName; import com.intellij.util.ArrayUtil; import com.intellij.util.ProcessingContext; -import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; -import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; -import com.jetbrains.python.psi.*; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.python.psi.AccessDirection; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyUtil; import com.jetbrains.python.psi.impl.PyImportedModule; +import com.jetbrains.python.psi.resolve.PointInImport; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.RatedResolveResult; import com.jetbrains.python.psi.resolve.ResolveImportUtil; @@ -35,6 +38,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; /** * @author yole @@ -58,33 +62,37 @@ public class PyImportedModuleType implements PyType { return new PyModuleType(file, myImportedModule).resolveMember(name, location, direction, resolveContext); } else if (resolved instanceof PsiDirectory) { - final List elements = Collections.singletonList(ResolveImportUtil.resolveChild(resolved, name, null, true, true)); + List elements = Collections.singletonList(ResolveImportUtil.resolveChild(resolved, name, null, true, true)); + if (location != null && ResolveImportUtil.getPointInImport(location) == PointInImport.NONE) { + final Set imported = Sets.newHashSet(PyModuleType.collectImportedSubmodules((PsiDirectory)resolved, location)); + elements = ContainerUtil.filter(elements, new Condition() { + @Override + public boolean value(PsiElement element) { + return imported.contains(element); + } + }); + } return ResolveImportUtil.rateResults(elements); } return null; } public Object[] getCompletionVariants(String completionPrefix, PsiElement location, ProcessingContext context) { - List result = new ArrayList(); - ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(location); - assert scopeOwner != null; - final List importTargets = PyModuleType.getVisibleImports(scopeOwner); - final int imported = myImportedModule.getImportedPrefix().getComponentCount(); - for (PyImportElement importTarget : importTargets) { - final QualifiedName qName = importTarget.getImportedQName(); - if (qName != null && qName.matchesPrefix(myImportedModule.getImportedPrefix())) { - final List components = qName.getComponents(); - if (components.size() > imported) { - String module = components.get(imported); - result.add(LookupElementBuilder.create(module)); - } - } - } + final List result = new ArrayList(); final PsiElement resolved = myImportedModule.resolve(); - if (resolved instanceof PsiDirectory) { + if (resolved instanceof PyFile) { + final PyModuleType moduleType = new PyModuleType((PyFile)resolved, myImportedModule); + result.addAll(moduleType.getCompletionVariantsAsLookupElements(location, context, false, false)); + } + else if (resolved instanceof PsiDirectory) { final PsiDirectory dir = (PsiDirectory)resolved; if (PyUtil.isPackage(dir, location)) { - result.addAll(PyModuleType.getSubModuleVariants(dir, location, null)); + if (ResolveImportUtil.getPointInImport(location) != PointInImport.NONE) { + result.addAll(PyModuleType.getSubModuleVariants(dir, location, null)); + } + else { + result.addAll(PyModuleType.collectImportedSubmodulesAsLookupElements(dir, location, context.get(CTX_NAMES))); + } } } return ArrayUtil.toObjectArray(result); diff --git a/python/src/com/jetbrains/python/psi/types/PyModuleType.java b/python/src/com/jetbrains/python/psi/types/PyModuleType.java index de3be514462f..3fedadfd29c4 100644 --- a/python/src/com/jetbrains/python/psi/types/PyModuleType.java +++ b/python/src/com/jetbrains/python/psi/types/PyModuleType.java @@ -29,7 +29,9 @@ import com.intellij.psi.*; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.QualifiedName; +import com.intellij.util.Function; import com.intellij.util.ProcessingContext; +import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.PyCustomMember; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; @@ -331,7 +333,7 @@ public class PyModuleType implements PyType { // Modules don't descend from obje result.addAll(getSubModuleVariants(myModule.getContainingDirectory(), location, namesAlready)); } else { - addImportedSubmodules(location, namesAlready, result); + result.addAll(collectImportedSubmodulesAsLookupElements(myModule, location, namesAlready)); } } return result; @@ -355,27 +357,62 @@ public class PyModuleType implements PyType { // Modules don't descend from obje return processor; } - private void addImportedSubmodules(PsiElement location, Set existingNames, List result) { - PsiFile file = location.getContainingFile(); - if (file instanceof PyFile) { - PyFile pyFile = (PyFile)file; - PsiElement moduleBase = PyUtil.isPackage(myModule) ? myModule.getContainingDirectory() : myModule; - for (PyImportElement importElement : pyFile.getImportTargets()) { - PsiElement target = PyUtil.turnInitIntoDir(importElement.resolve()); - if (target != null && PsiTreeUtil.isAncestor(moduleBase, target, true)) { - LookupElement element = null; - if (target instanceof PsiFileSystemItem) { - element = buildFileLookupElement((PsiFileSystemItem) target, existingNames); - } - else if (target instanceof PsiNamedElement) { - element = LookupElementBuilder.createWithIcon((PsiNamedElement)target); - } - if (element != null) { - result.add(element); - } - } - } + @NotNull + public static List collectImportedSubmodulesAsLookupElements(@NotNull PsiFileSystemItem pyPackage, + @NotNull PsiElement location, + @Nullable final Set existingNames) { + + return ContainerUtil.mapNotNull(collectImportedSubmodules(pyPackage, location), + new Function() { + @Override + public LookupElement fun(PsiElement element) { + if (element instanceof PsiFileSystemItem) { + return buildFileLookupElement((PsiFileSystemItem)element, existingNames); + } + else if (element instanceof PsiNamedElement) { + return LookupElementBuilder.createWithIcon((PsiNamedElement)element); + } + return null; + } + }); + } + + @NotNull + public static List collectImportedSubmodules(@NotNull PsiFileSystemItem pyPackage, @NotNull PsiElement location) { + final PsiElement parentAnchor; + if (pyPackage instanceof PyFile && PyUtil.isPackage(((PyFile)pyPackage))) { + parentAnchor = ((PyFile)pyPackage).getContainingDirectory(); } + else if (pyPackage instanceof PsiDirectory && PyUtil.isPackage(((PsiDirectory)pyPackage), location)) { + parentAnchor = pyPackage; + } + else { + throw new IllegalArgumentException("Package argument should be either standard Python package or namespace package"); + } + + final ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(location); + if (scopeOwner == null) { + return Collections.emptyList(); + } + final List result = new ArrayList(); + nextImportElement: + for (PyImportElement importElement : getVisibleImports(scopeOwner)) { + PsiElement resolvedChild = PyUtil.turnInitIntoDir(importElement.resolve()); + if (resolvedChild == null || !PsiTreeUtil.isAncestor(parentAnchor, resolvedChild, true)) { + continue; + } + QualifiedName importedQName = importElement.getImportedQName(); + // Looking for strict child of parentAncestor + while (resolvedChild != null && resolvedChild.getParent() != parentAnchor) { + if (importedQName == null || importedQName.getComponentCount() <= 1) { + continue nextImportElement; + } + importedQName = importedQName.removeTail(1); + resolvedChild = PyUtil.turnInitIntoDir(ResolveImportUtil.resolveImportElement(importElement, importedQName)); + } + ContainerUtil.addIfNotNull(result, resolvedChild); + } + return result; } public static List getSubModuleVariants(final PsiDirectory directory, diff --git a/python/testData/completion/attributeOfIndirectlyImportedPackage/a.after.py b/python/testData/completion/attributeOfIndirectlyImportedPackage/a.after.py new file mode 100644 index 000000000000..f217a2117a34 --- /dev/null +++ b/python/testData/completion/attributeOfIndirectlyImportedPackage/a.after.py @@ -0,0 +1,3 @@ +import pkg1.m1 + +pkg1.foo \ No newline at end of file diff --git a/python/testData/completion/attributeOfIndirectlyImportedPackage/a.py b/python/testData/completion/attributeOfIndirectlyImportedPackage/a.py new file mode 100644 index 000000000000..03e922263bd6 --- /dev/null +++ b/python/testData/completion/attributeOfIndirectlyImportedPackage/a.py @@ -0,0 +1,3 @@ +import pkg1.m1 + +pkg1.f \ No newline at end of file diff --git a/python/testData/completion/attributeOfIndirectlyImportedPackage/pkg1/__init__.py b/python/testData/completion/attributeOfIndirectlyImportedPackage/pkg1/__init__.py new file mode 100644 index 000000000000..79bf605b3de4 --- /dev/null +++ b/python/testData/completion/attributeOfIndirectlyImportedPackage/pkg1/__init__.py @@ -0,0 +1 @@ +foo = 42 \ No newline at end of file diff --git a/python/testData/completion/attributeOfIndirectlyImportedPackage/pkg1/m1.py b/python/testData/completion/attributeOfIndirectlyImportedPackage/pkg1/m1.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/notImportedSubmodulesOfNamespacePackage/a.after.py b/python/testData/completion/notImportedSubmodulesOfNamespacePackage/a.after.py new file mode 100644 index 000000000000..8d04a72755a1 --- /dev/null +++ b/python/testData/completion/notImportedSubmodulesOfNamespacePackage/a.after.py @@ -0,0 +1,3 @@ +import nspkg1.m2 + +nspkg1.m2 diff --git a/python/testData/completion/notImportedSubmodulesOfNamespacePackage/a.py b/python/testData/completion/notImportedSubmodulesOfNamespacePackage/a.py new file mode 100644 index 000000000000..1685a9106112 --- /dev/null +++ b/python/testData/completion/notImportedSubmodulesOfNamespacePackage/a.py @@ -0,0 +1,3 @@ +import nspkg1.m2 + +nspkg1.m diff --git a/python/testData/completion/notImportedSubmodulesOfNamespacePackage/nspkg1/m2.py b/python/testData/completion/notImportedSubmodulesOfNamespacePackage/nspkg1/m2.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/notImportedSubmodulesOfNamespacePackage/nspkg1/m3.py b/python/testData/completion/notImportedSubmodulesOfNamespacePackage/nspkg1/m3.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/submoduleOfIndirectlyImportedPackage/a.py b/python/testData/completion/submoduleOfIndirectlyImportedPackage/a.py new file mode 100644 index 000000000000..496be75feba7 --- /dev/null +++ b/python/testData/completion/submoduleOfIndirectlyImportedPackage/a.py @@ -0,0 +1,4 @@ +import pkg1.subpkg1.m1 +import pkg1 + +pkg1. \ No newline at end of file diff --git a/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/__init__.py b/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/__init__.py new file mode 100644 index 000000000000..fe5433bef744 --- /dev/null +++ b/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/__init__.py @@ -0,0 +1 @@ +VAR = 42 \ No newline at end of file diff --git a/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/subpkg1/__init__.py b/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/subpkg1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/subpkg1/m1.py b/python/testData/completion/submoduleOfIndirectlyImportedPackage/pkg1/subpkg1/m1.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/main.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/main.py new file mode 100644 index 000000000000..54e2273f4550 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/main.py @@ -0,0 +1,5 @@ +import nspkg1.m2 + +print(nspkg1.m2) +print(nspkg1.m3) +print(nspkg1.nssubpkg1) diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/nspkg1/m2.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/nspkg1/m2.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/nspkg1/m3.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/nspkg1/m3.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/nspkg1/nssubpkg1/m1.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NotImportedSubmodulesOfNamespacePackage/nspkg1/nssubpkg1/m1.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java index 7e085a397a2e..51b463948b03 100644 --- a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java +++ b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java @@ -136,4 +136,9 @@ public class Py3CompletionTest extends PyTestCase { myFixture.completeBasic(); myFixture.checkResultByFile("completion/" + getTestName(true) + "/nspkg1/a.after.py"); } + + // PY-14385 + public void testNotImportedSubmodulesOfNamespacePackage() { + doMultiFileTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java index a1366ac2e17a..c6a66f4f9fb0 100644 --- a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java @@ -707,4 +707,19 @@ public class PythonCompletionTest extends PyTestCase { assertContainsElements(suggested, PyNames.FUNCTION_SPECIAL_ATTRIBUTES); assertDoesntContain(suggested, PyNames.METHOD_SPECIAL_ATTRIBUTES); } + + // PY-14388 + public void testAttributeOfIndirectlyImportedPackage() { + doMultiFileTest(); + } + + // PY-14387 + public void testSubmoduleOfIndirectlyImportedPackage() { + myFixture.copyDirectoryToProject("completion/" + getTestName(true), ""); + myFixture.configureByFile("a.py"); + myFixture.completeBasic(); + final List suggested = myFixture.getLookupElementStrings(); + assertNotNull(suggested); + assertSameElements(suggested, "VAR", "subpkg1"); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index 159a7218ff6f..32505db617a1 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -111,4 +111,9 @@ public class Py3UnresolvedReferencesInspectionTest extends PyTestCase { public void testAttributesOfUnresolvedTypeFile() { doTest(); } + + // PY-14385 + public void testNotImportedSubmodulesOfNamespacePackage() { + doMultiFileTest("main.py"); + } }