diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 42c11a827172..02dbacbf2c83 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -475,7 +475,7 @@ + implementationClass="com.jetbrains.python.codeInsight.completion.PyModulePackageCompletionContributor"/> getFunctionInsertHandler(PsiElement parent) { + private InsertHandler getFunctionInsertHandler(PsiElement parent) { if (parent instanceof PyStringLiteralExpression) { - return STRING_LITERAL_INSERT_HANDLER; + return getStringLiteralInsertHandler(); } if (parent.getParent() instanceof PyDecorator) { - return IMPORTING_INSERT_HANDLER; + return getImportingInsertHandler(); } - return FUNCTION_INSERT_HANDLER; + return getFunctionInsertHandler(); } - private static final Condition IS_TOPLEVEL = element -> PyUtil.isTopLevel(element); - private static void addVariantsFromIndex(@NotNull CompletionResultSet resultSet, @NotNull PsiFile targetFile, @NotNull StubIndexKey key, @@ -150,51 +133,4 @@ public class PyClassNameCompletionContributor extends CompletionContributor { .map(elementHandler) .forEach(resultSet::addElement); } - - static final InsertHandler IMPORTING_INSERT_HANDLER = new InsertHandler() { - public void handleInsert(final InsertionContext context, final LookupElement item) { - addImportForLookupElement(context, item, context.getTailOffset() - 1); - } - }; - - - static final InsertHandler FUNCTION_INSERT_HANDLER = new PyFunctionInsertHandler() { - public void handleInsert(@NotNull final InsertionContext context, @NotNull final LookupElement item) { - int tailOffset = context.getTailOffset()-1; - super.handleInsert(context, item); // adds parentheses, modifies tail offset - context.commitDocument(); - addImportForLookupElement(context, item, tailOffset); - } - }; - - static final InsertHandler STRING_LITERAL_INSERT_HANDLER = new InsertHandler() { - @Override - public void handleInsert(InsertionContext context, LookupElement item) { - PsiElement element = item.getPsiElement(); - if (element instanceof PyQualifiedNameOwner) { - String qName = ((PyQualifiedNameOwner) element).getQualifiedName(); - String name = ((PyQualifiedNameOwner) element).getName(); - if (qName != null && name != null) { - String qNamePrefix = qName.substring(0, qName.length()-name.length()); - context.getDocument().insertString(context.getStartOffset(), qNamePrefix); - } - } - } - }; - - private static void addImportForLookupElement(final InsertionContext context, final LookupElement item, final int tailOffset) { - PsiDocumentManager manager = PsiDocumentManager.getInstance(context.getProject()); - Document document = manager.getDocument(context.getFile()); - if (document != null) { - manager.commitDocument(document); - } - final PsiReference ref = context.getFile().findReferenceAt(tailOffset); - if (ref == null || ref.resolve() == item.getPsiElement()) { - // no import statement needed - return; - } - WriteCommandAction.writeCommandAction(context.getProject(), context.getFile()).run(() -> { - AddImportHelper.addImport(PyUtil.as(item.getPsiElement(), PsiNamedElement.class), context.getFile(), (PyElement)ref.getElement()); - }); - } } diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyExtendedCompletionContributor.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyExtendedCompletionContributor.kt new file mode 100644 index 000000000000..7b55f2393790 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyExtendedCompletionContributor.kt @@ -0,0 +1,103 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.codeInsight.completion + +import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.psi.MultiplePsiFilesPerDocumentFileViewProvider +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.codeInsight.imports.AddImportHelper +import com.jetbrains.python.psi.* + +/** + * Provides basic functionality for extended completion. + * + * Extended code completion is actually a basic code completion that shows the names of classes, functions, modules and variables. + * + * To provide variants for extended completion override [doFillCompletionVariants] + */ +abstract class PyExtendedCompletionContributor : CompletionContributor() { + + protected val importingInsertHandler: InsertHandler = InsertHandler { context, item -> + addImportForLookupElement(context, item, context.tailOffset - 1) + } + + protected val functionInsertHandler: InsertHandler = object : PyFunctionInsertHandler() { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val tailOffset = context.tailOffset - 1 + super.handleInsert(context, item) // adds parentheses, modifies tail offset + context.commitDocument() + addImportForLookupElement(context, item, tailOffset) + } + } + + protected val stringLiteralInsertHandler: InsertHandler = InsertHandler { context, item -> + val element = item.psiElement + if (element is PyQualifiedNameOwner) { + val qName = element.qualifiedName + val name = element.name + if (qName != null && name != null) { + val qNamePrefix = qName.substring(0, qName.length - name.length) + context.document.insertString(context.startOffset, qNamePrefix) + } + } + } + + /** + * Checks whether completion should be performed for a given [parameters] and delegates actual work to [doFillCompletionVariants]. + */ + final override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { + if (!shouldDoCompletion(parameters)) return + doFillCompletionVariants(parameters, result) + } + + /** + * Subclasses should override the method to provide completion variants. + */ + protected abstract fun doFillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) + + private fun shouldDoCompletion(parameters: CompletionParameters): Boolean { + if (!parameters.isExtendedCompletion) { + return false + } + + val element = parameters.position + val parent = element.parent + if (parent is PyReferenceExpression && parent.isQualified) { + return false + } + if (parent is PyStringLiteralExpression) { + val prefix = parent.text.substring(0, parameters.offset - parent.textRange.startOffset) + if (prefix.contains(".")) { + return false + } + } + val provider = element.containingFile.viewProvider + if (provider is MultiplePsiFilesPerDocumentFileViewProvider) { + return false + } + + return PsiTreeUtil.getParentOfType(element, PyImportStatementBase::class.java) == null + } +} + +private fun addImportForLookupElement(context: InsertionContext, item: LookupElement, tailOffset: Int) { + val manager = PsiDocumentManager.getInstance(context.project) + val document = manager.getDocument(context.file) + if (document != null) { + manager.commitDocument(document) + } + val ref = context.file.findReferenceAt(tailOffset) + if (ref == null || ref.resolve() === item.psiElement) { + // no import statement needed + return + } + WriteCommandAction.writeCommandAction(context.project, context.file).run { + val psiElement = item.psiElement + if (psiElement is PsiNamedElement) { + AddImportHelper.addImport(psiElement, context.file, ref.element as PyElement) + } + } +} diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyModuleCompletionContributor.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyModulePackageCompletionContributor.kt similarity index 65% rename from python/src/com/jetbrains/python/codeInsight/completion/PyModuleCompletionContributor.kt rename to python/src/com/jetbrains/python/codeInsight/completion/PyModulePackageCompletionContributor.kt index 96c890b26ef1..e98f48fd5c02 100644 --- a/python/src/com/jetbrains/python/codeInsight/completion/PyModuleCompletionContributor.kt +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyModulePackageCompletionContributor.kt @@ -1,25 +1,28 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.codeInsight.completion -import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionResultSet -import com.jetbrains.python.codeInsight.completion.PyClassNameCompletionContributor.IMPORTING_INSERT_HANDLER -import com.jetbrains.python.codeInsight.completion.PyClassNameCompletionContributor.STRING_LITERAL_INSERT_HANDLER +import com.intellij.psi.PsiFileSystemItem import com.jetbrains.python.codeInsight.imports.PythonImportUtils import com.jetbrains.python.psi.PyFile import com.jetbrains.python.psi.PyStringLiteralExpression -import com.jetbrains.python.psi.impl.PyPsiFacadeImpl import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext import com.jetbrains.python.psi.resolve.QualifiedNameFinder +import com.jetbrains.python.psi.resolve.fromFoothold import com.jetbrains.python.psi.resolve.resolveQualifiedName import com.jetbrains.python.psi.stubs.PyModuleNameIndex import com.jetbrains.python.psi.types.PyModuleType -class PyModuleCompletionContributor : CompletionContributor() { +/** + * Add completion variants for modules and packages. + * + * The completion contributor ensures that completion variants are resolvable with project source root configuration. + * The list of completion variants does not include namespace packages (but includes their modules where appropriate). + */ +class PyModulePackageCompletionContributor : PyExtendedCompletionContributor() { - override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { - if (!parameters.isExtendedCompletion) return + override fun doFillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { val targetFile = parameters.originalFile val inStringLiteral = parameters.position.parent is PyStringLiteralExpression @@ -27,23 +30,24 @@ class PyModuleCompletionContributor : CompletionContributor() { val modulesFromIndex = moduleKeys.asSequence() .filter { result.prefixMatcher.prefixMatches(it) } .flatMap { PyModuleNameIndex.find(it, targetFile.project, true).asSequence() } + .toList() - val resolveContext = PyPsiFacadeImpl(targetFile.project).createResolveContextFromFoothold(targetFile) - val builders = modulesFromIndex + val resolveContext = fromFoothold(targetFile) + val builders = modulesFromIndex.asSequence() .flatMap { resolve(it, resolveContext) } .filter { PythonImportUtils.isImportableModule(targetFile, it) } .mapNotNull { PyModuleType.buildFileLookupElement(it, null) } .map { it.withInsertHandler( - if (inStringLiteral) STRING_LITERAL_INSERT_HANDLER else IMPORTING_INSERT_HANDLER) + if (inStringLiteral) stringLiteralInsertHandler else importingInsertHandler) } builders.forEach { result.addElement(it) } } - private fun resolve(module: PyFile, resolveContext: PyQualifiedNameResolveContext): Sequence { + private fun resolve(module: PyFile, resolveContext: PyQualifiedNameResolveContext): Sequence { val qualifiedName = QualifiedNameFinder.findCanonicalImportPath(module, null) ?: return emptySequence() return resolveQualifiedName(qualifiedName, resolveContext).asSequence() - .filterIsInstance() + .filterIsInstance() } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/codeInsight/imports/PythonImportUtils.java b/python/src/com/jetbrains/python/codeInsight/imports/PythonImportUtils.java index 8a08c4d4d7b3..0425a96abf0a 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/PythonImportUtils.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/PythonImportUtils.java @@ -34,6 +34,7 @@ import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.inspections.unresolvedReference.PyPackageAliasesProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyFileImpl; +import com.jetbrains.python.psi.resolve.PyResolveImportUtil; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.psi.search.PyProjectScopeBuilder; import com.jetbrains.python.psi.stubs.PyClassNameIndex; @@ -254,9 +255,9 @@ public final class PythonImportUtils { public static boolean isImportableModule(PsiFile targetFile, @NotNull PsiFileSystemItem file) { PsiDirectory parent = (PsiDirectory)file.getParent(); return parent != null && file != targetFile && - (parent.findFile(PyNames.INIT_DOT_PY) != null || - ImportFromExistingAction.isRoot(parent) || - parent == targetFile.getParent()); + (ImportFromExistingAction.isRoot(parent) || + parent == targetFile.getParent() || + PyUtil.isPackage(parent, false, null)); } private static boolean isIndexableTopLevel(PsiElement symbol) { diff --git a/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 3b937b89a03f..30d4cd959860 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -301,8 +301,9 @@ public class PyFunctionImpl extends PyBaseElementImpl implements @Override public String getLocationString() { final PyClass containingClass = getContainingClass(); - if (containingClass != null) { - return "(" + containingClass.getName() + " in " + getPackageForFile(getContainingFile()) + ")"; + final String packageForFile = getPackageForFile(getContainingFile()); + if (containingClass != null && packageForFile != null) { + return String.format("(%s in %s)", containingClass.getName(), packageForFile); } return super.getLocationString(); } diff --git a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java index 6f54cfd10327..1e79fa28276d 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java @@ -341,7 +341,8 @@ public class PyStringLiteralExpressionImpl extends PyElementImpl implements PySt @Nullable @Override public String getLocationString() { - return "(" + PyElementPresentation.getPackageForFile(getContainingFile()) + ")"; + String packageForFile = PyElementPresentation.getPackageForFile(getContainingFile()); + return packageForFile != null ? String.format("(%s)", packageForFile) : null; } @Nullable diff --git a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java index 8e9dca09c7d4..21511ef64aee 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java @@ -705,8 +705,9 @@ public class PyTargetExpressionImpl extends PyBaseElementImpl \ No newline at end of file diff --git a/python/testData/completion/fromPackageImport/pkg/__init__.py b/python/testData/completion/fromPackageImport/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/fromPackageImport/pkg/m1.py b/python/testData/completion/fromPackageImport/pkg/m1.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/fromPackageImport/pkg/m2.py b/python/testData/completion/fromPackageImport/pkg/m2.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/moduleFromMultipleSourceRoots/a.after.py b/python/testData/completion/moduleFromMultipleSourceRoots/a.after.py new file mode 100644 index 000000000000..dcaab9e38faf --- /dev/null +++ b/python/testData/completion/moduleFromMultipleSourceRoots/a.after.py @@ -0,0 +1,3 @@ +import m7 + +m7 \ No newline at end of file diff --git a/python/testData/completion/moduleFromNamespacePackage/a.after.py b/python/testData/completion/moduleFromNamespacePackage/a.after.py new file mode 100644 index 000000000000..2b5bbf3d5838 --- /dev/null +++ b/python/testData/completion/moduleFromNamespacePackage/a.after.py @@ -0,0 +1,3 @@ +from pkg import m7 + +m7 \ No newline at end of file diff --git a/python/testData/completion/moduleFromNamespacePackage/a.py b/python/testData/completion/moduleFromNamespacePackage/a.py new file mode 100644 index 000000000000..7fe1bb3d9c19 --- /dev/null +++ b/python/testData/completion/moduleFromNamespacePackage/a.py @@ -0,0 +1 @@ +m7 \ No newline at end of file diff --git a/python/testData/completion/moduleFromNamespacePackage/pkg/m7.py b/python/testData/completion/moduleFromNamespacePackage/pkg/m7.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/packageFromMultipleSourceRoots/a.after.py b/python/testData/completion/packageFromMultipleSourceRoots/a.after.py new file mode 100644 index 000000000000..1dc979644d6a --- /dev/null +++ b/python/testData/completion/packageFromMultipleSourceRoots/a.after.py @@ -0,0 +1,3 @@ +import pkg1 + +pkg1 \ No newline at end of file diff --git a/python/testData/completion/packageFromMultipleSourceRoots/a.py b/python/testData/completion/packageFromMultipleSourceRoots/a.py new file mode 100644 index 000000000000..eb0efc4fd781 --- /dev/null +++ b/python/testData/completion/packageFromMultipleSourceRoots/a.py @@ -0,0 +1 @@ +pkg1 \ No newline at end of file diff --git a/python/testData/completion/packageFromMultipleSourceRoots/root1/pkg1/__init__.py b/python/testData/completion/packageFromMultipleSourceRoots/root1/pkg1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/completion/packageFromMultipleSourceRoots/root2/pkg1/__init__.py b/python/testData/completion/packageFromMultipleSourceRoots/root2/pkg1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java index 40846a68d9fe..565751ec7a07 100644 --- a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java +++ b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java @@ -2,7 +2,6 @@ package com.jetbrains.python; import com.google.common.collect.Lists; -import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.vfs.VfsUtilCore; @@ -10,7 +9,6 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.testFramework.PlatformTestUtil; -import com.intellij.testFramework.PsiTestUtil; import com.jetbrains.python.codeInsight.PyCustomMember; import com.jetbrains.python.fixtures.PyMultiFileResolveTestCase; import com.jetbrains.python.fixtures.PyResolveTestCase; @@ -438,7 +436,7 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { // PY-18626 public void testManySourceRoots() { myFixture.copyDirectoryToProject("manySourceRoots", ""); - withSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root1"), myFixture.findFileInTempDir("root2")), () -> { + runWithSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root1"), myFixture.findFileInTempDir("root2")), () -> { final PsiFile psiFile = myFixture.configureByFile("a.py"); final PsiReference ref = PyResolveTestCase.findReferenceByMarker(psiFile); assertInstanceOf(ref, PsiPolyVariantReference.class); @@ -454,7 +452,7 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { // PY-28321 public void testImportManySourceRoots() { myFixture.copyDirectoryToProject("importManySourceRoots", ""); - withSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root2"), myFixture.findFileInTempDir("root1")), () -> { + runWithSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root2"), myFixture.findFileInTempDir("root1")), () -> { final PsiFile psiFile = myFixture.configureByFile("root1/pkg/a.py"); final PsiReference ref = PyResolveTestCase.findReferenceByMarker(psiFile); assertInstanceOf(ref, PsiPolyVariantReference.class); @@ -470,7 +468,7 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { // PY-28321 public void testImportManySourceRootsReverseRootOrder() { myFixture.copyDirectoryToProject("importManySourceRoots", ""); - withSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root1"), myFixture.findFileInTempDir("root2")), () -> { + runWithSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root1"), myFixture.findFileInTempDir("root2")), () -> { final PsiFile psiFile = myFixture.configureByFile("root1/pkg/a.py"); final PsiReference ref = PyResolveTestCase.findReferenceByMarker(psiFile); assertInstanceOf(ref, PsiPolyVariantReference.class); @@ -485,7 +483,7 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { VirtualFile vf = myFixture.findFileInTempDir("ext/m1.py"); - withSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root")), () -> { + runWithSourceRoots(Lists.newArrayList(myFixture.findFileInTempDir("root")), () -> { final PsiFile extSource = myFixture.getPsiManager().findFile(vf); PyImportResolver foreignResolver = (name, context, withRoots) -> name.toString().equals("m1") ? extSource : null; PlatformTestUtil.registerExtension(PyImportResolver.EP_NAME, foreignResolver, getTestRootDisposable()); diff --git a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java index 7725ef90f0e0..8f8890f7be6b 100644 --- a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java @@ -10,7 +10,7 @@ import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.testFramework.TestDataPath; import com.jetbrains.python.documentation.docstrings.DocStringFormat; -import com.jetbrains.python.fixtures.PyMultiFileResolveTestCase; +import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.psi.LanguageLevel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,7 +21,7 @@ import java.util.List; import java.util.Set; @TestDataPath("$CONTENT_ROOT/../testData/completion") -public class PythonCompletionTest extends PyMultiFileResolveTestCase { +public class PythonCompletionTest extends PyTestCase { private void doTest() { CamelHumpMatcher.forceStartMatching(myFixture.getTestRootDisposable()); @@ -32,9 +32,13 @@ public class PythonCompletionTest extends PyMultiFileResolveTestCase { } private void doMultiFileTest() { + doMultiFileTest(CompletionType.BASIC, 1); + } + + private void doMultiFileTest(CompletionType completionType, int invocationCount) { myFixture.copyDirectoryToProject(getTestName(true), ""); myFixture.configureByFile("a.py"); - myFixture.completeBasic(); + myFixture.complete(completionType, invocationCount); myFixture.checkResultByFile(getTestName(true) + "/a.after.py"); } @@ -1288,7 +1292,7 @@ public class PythonCompletionTest extends PyMultiFileResolveTestCase { // PY-17810 public void testDuplicatedEntriesFromMultipleSourceRoots() { - assertSingleVariantInExtendedCompletion(); + assertSingleVariantInExtendedCompletionWithSourceRoots(); } // PY-17810 @@ -1298,7 +1302,27 @@ public class PythonCompletionTest extends PyMultiFileResolveTestCase { // PY-17810 public void testModuleFromMultipleSourceRoots() { - assertSingleVariantInExtendedCompletion(); + assertSingleVariantInExtendedCompletionWithSourceRoots(); + } + + // PY-17810 + public void testPackageFromMultipleSourceRoots() { + assertSingleVariantInExtendedCompletionWithSourceRoots(); + } + + // PY-17810 + public void testFromPackageImport() { + myFixture.copyDirectoryToProject(getTestName(true), ""); + myFixture.configureByFile("a.py"); + myFixture.complete(CompletionType.BASIC, 2); + final List suggested = myFixture.getLookupElementStrings(); + assertNotNull(suggested); + assertSameElements(suggested, "m1", "m2"); + } + + // PY-28989 + public void testModuleFromNamespacePackage() { + runWithLanguageLevel(LanguageLevel.PYTHON34, this::assertSingleVariantInExtendedCompletion); } private void assertNoVariantsInExtendedCompletion() { @@ -1310,15 +1334,20 @@ public class PythonCompletionTest extends PyMultiFileResolveTestCase { } private void assertSingleVariantInExtendedCompletion() { + doMultiFileTest(CompletionType.BASIC, 2); + assertNull(myFixture.getLookupElements()); + } + + private void assertSingleVariantInExtendedCompletionWithSourceRoots() { myFixture.copyDirectoryToProject(getTestName(true), ""); - withSourceRoots(Lists.newArrayList( + runWithSourceRoots(Lists.newArrayList( myFixture.findFileInTempDir("root1"), myFixture.findFileInTempDir("root2")), - () -> { - myFixture.configureByFile("a.py"); - myFixture.complete(CompletionType.BASIC, 2); - assertNull(myFixture.getLookupElements()); - }); + () -> { + myFixture.configureByFile("a.py"); + assertNull(myFixture.complete(CompletionType.BASIC, 2)); + myFixture.checkResultByFile(getTestName(true) + "/a.after.py"); + }); } @Override diff --git a/python/testSrc/com/jetbrains/python/fixtures/PyMultiFileResolveTestCase.java b/python/testSrc/com/jetbrains/python/fixtures/PyMultiFileResolveTestCase.java index 078c55450aee..0af16969f377 100644 --- a/python/testSrc/com/jetbrains/python/fixtures/PyMultiFileResolveTestCase.java +++ b/python/testSrc/com/jetbrains/python/fixtures/PyMultiFileResolveTestCase.java @@ -16,12 +16,10 @@ package com.jetbrains.python.fixtures; import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.module.Module; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileFilter; import com.intellij.psi.*; import com.intellij.psi.impl.PsiManagerEx; -import com.intellij.testFramework.PsiTestUtil; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PythonFileType; import com.jetbrains.python.PythonTestUtil; @@ -100,13 +98,4 @@ public abstract class PyMultiFileResolveTestCase extends PyResolveTestCase { return Collections.singletonList(ref.resolve()); } - protected void withSourceRoots(@NotNull List sourceRoots, @NotNull Runnable f) { - final Module module = myFixture.getModule(); - sourceRoots.forEach(root -> PsiTestUtil.addSourceRoot(module, root)); - try { - f.run(); - } finally { - sourceRoots.forEach(root -> PsiTestUtil.removeSourceRoot(module, root)); - } - } } diff --git a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java index 7413efeb56ab..a2c3e4af757e 100644 --- a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java +++ b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java @@ -205,10 +205,10 @@ public abstract class PyTestCase extends UsefulTestCase { PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), languageLevel); } - protected void runWithLanguageLevel(@NotNull LanguageLevel languageLevel, @NotNull Runnable action) { + protected void runWithLanguageLevel(@NotNull LanguageLevel languageLevel, @NotNull Runnable runnable) { setLanguageLevel(languageLevel); try { - action.run(); + runnable.run(); } finally { setLanguageLevel(null); @@ -227,6 +227,16 @@ public abstract class PyTestCase extends UsefulTestCase { } } + protected void runWithSourceRoots(@NotNull List sourceRoots, @NotNull Runnable runnable) { + final Module module = myFixture.getModule(); + sourceRoots.forEach(root -> PsiTestUtil.addSourceRoot(module, root)); + try { + runnable.run(); + } finally { + sourceRoots.forEach(root -> PsiTestUtil.removeSourceRoot(module, root)); + } + } + protected static void assertNotParsed(PsiFile file) { assertInstanceOf(file, PyFileImpl.class); assertNull("Operations should have been performed on stubs but caused file to be parsed: " + file.getVirtualFile().getPath(),