diff --git a/python/pluginResources/inspectionDescriptions/PyRelativeImportInspection.html b/python/pluginResources/inspectionDescriptions/PyRelativeImportInspection.html new file mode 100644 index 000000000000..bcb0d791a590 --- /dev/null +++ b/python/pluginResources/inspectionDescriptions/PyRelativeImportInspection.html @@ -0,0 +1,5 @@ + + +This inspection reports usages of relative imports inside plain directories, i.e. directories neither containing __init__.py nor explicitly marked as namespace packages. + + \ No newline at end of file diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index faf68d1afb80..b1270c365e6e 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -23,6 +23,10 @@ QFIX.generate.binary.stubs=Generate binary stubs QFIX.reformat.file=Reformat file QFIX.bold.html.text={0} +# Namespace packages +QFIX.mark.as.namespace.package=Mark directory ''{0}'' as namespace package +QFIX.change.to.same.directory.import=Change to same directory import + # Intentions: INTN # Conflict checker @@ -1207,3 +1211,14 @@ goto.superclass.choose=Choose superclass python.symbol.chooser.dialog.title=Choose Symbol button.install.to.user.site.packages.directory=Install to user's site packages directory framework.not.supported.for.the.selected.interpreter={0} is not supported for the selected interpreter + +action.MarkNamespacePackageDirectory.text=Namespace package +python.unmark.as.namespace.package=Unmark as Namespace Package +python.namespace.package.folder=Namespace Package +python.namespace.packages.name=Namespace packages +python.namespace.packages.description=Namespace Package Folders +dialog.title.can.t.create.package=Create Package Error +command.name.create.new.package=Create New Package {0} +dialog.title.new.python.package=New Python Package +new.package.list.item.ordinary.package=Package +new.package.list.item.namespace.package=Namespace package \ No newline at end of file diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 828846dd7611..7b767a61cbc4 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1073,3 +1073,7 @@ python.find.usages.usage.in.isinstance=Usage in isinstance() python.find.usages.untyped.probable.usage=Untyped (probable) usage python.find.usages.usage.in.import.statement=Usage in import statement +# PyPackagesInspection +INSP.NAME.relative.import=Suspicious relative import +INSP.relative.import.relative.import.outside.package=Relative import outside of a package + diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java index f2695854e5d7..6b57ec92bd2d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java @@ -44,6 +44,7 @@ import com.jetbrains.python.psi.impl.PyTypeProvider; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.psi.resolve.RatedResolveResult; +import com.jetbrains.python.psi.resolve.ResolveImportUtil; import com.jetbrains.python.psi.stubs.PySetuptoolsNamespaceIndex; import com.jetbrains.python.psi.types.*; import one.util.streamex.StreamEx; @@ -813,10 +814,15 @@ public final class PyUtil { if (directory == null) return true; VirtualFile vFile = directory.getVirtualFile(); if (vFile == null) return true; - ProjectFileIndex fileIndex = ProjectFileIndex.SERVICE.getInstance(directory.getProject()); - return Comparing.equal(fileIndex.getClassRootForFile(vFile), vFile) || - Comparing.equal(fileIndex.getContentRootForFile(vFile), vFile) || - Comparing.equal(fileIndex.getSourceRootForFile(vFile), vFile); + Project project = directory.getProject(); + return isRoot(vFile, project); + } + + public static boolean isRoot(@NotNull VirtualFile directory, @NotNull Project project) { + ProjectFileIndex fileIndex = ProjectFileIndex.SERVICE.getInstance(project); + return Comparing.equal(fileIndex.getClassRootForFile(directory), directory) || + Comparing.equal(fileIndex.getContentRootForFile(directory), directory) || + Comparing.equal(fileIndex.getSourceRootForFile(directory), directory); } /** @@ -964,14 +970,7 @@ public final class PyUtil { * @see PyNames#isIdentifier(String) */ public static boolean isPackage(@NotNull PsiDirectory directory, boolean checkSetupToolsPackages, @Nullable PsiElement anchor) { - for (PyCustomPackageIdentifier customPackageIdentifier : PyCustomPackageIdentifier.EP_NAME.getExtensions()) { - if (customPackageIdentifier.isPackage(directory)) { - return true; - } - } - if (directory.findFile(PyNames.INIT_DOT_PY) != null) { - return true; - } + if (isExplicitPackage(directory)) return true; final LanguageLevel level = anchor != null ? LanguageLevel.forElement(anchor) : LanguageLevel.forElement(directory); if (!level.isPython2()) { return true; @@ -993,6 +992,19 @@ public final class PyUtil { anchor instanceof PsiDirectory && isPackage((PsiDirectory)anchor, location); } + public static boolean isCustomPackage(@NotNull PsiDirectory directory) { + for (PyCustomPackageIdentifier customPackageIdentifier : PyCustomPackageIdentifier.EP_NAME.getExtensions()) { + if (customPackageIdentifier.isPackage(directory)) { + return true; + } + } + return false; + } + + public static boolean isExplicitPackage(@NotNull PsiDirectory directory) { + return isOrdinaryPackage(directory) || isCustomPackage(directory); + } + private static boolean isSetuptoolsNamespacePackage(@NotNull PsiDirectory directory) { final String packagePath = getPackagePath(directory); return packagePath != null && !PySetuptoolsNamespaceIndex.find(packagePath, directory.getProject()).isEmpty(); @@ -1639,6 +1651,10 @@ public final class PyUtil { } } + public static boolean isOrdinaryPackage(@NotNull PsiDirectory directory) { + return directory.findFile(PyNames.INIT_DOT_PY) != null; + } + /** * This helper class allows to collect various information about AST nodes composing {@link PyStringLiteralExpression}. */ diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java index 68228d062a20..f2c83a7c39bb 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java @@ -8,6 +8,7 @@ import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.util.RecursionManager; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -35,6 +36,7 @@ import static com.jetbrains.python.psi.FutureFeature.ABSOLUTE_IMPORT; * @author dcheryasov */ public final class ResolveImportUtil { + private ResolveImportUtil() { } @@ -44,6 +46,9 @@ public final class ResolveImportUtil { if (file instanceof PyFile) { final PyFile pyFile = (PyFile)file; if (pyFile.getLanguageLevel().isPy3K()) { + if (foothold.getManager().isInProject(foothold) && Registry.is("python.explicit.namespace.packages")) { + return false; + } return true; } return pyFile.hasImportFromFuture(ABSOLUTE_IMPORT); diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index baf175fc625f..5cc504c91d43 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -161,6 +161,7 @@ + @@ -324,6 +325,11 @@ + + + + + @@ -361,6 +367,7 @@ + @@ -537,6 +544,9 @@ + + + @@ -632,6 +642,10 @@ + + + + diff --git a/python/src/com/jetbrains/python/PyDirectoryIconProvider.java b/python/src/com/jetbrains/python/PyDirectoryIconProvider.java index f2f73802dfd5..12abe1df5487 100644 --- a/python/src/com/jetbrains/python/PyDirectoryIconProvider.java +++ b/python/src/com/jetbrains/python/PyDirectoryIconProvider.java @@ -38,7 +38,8 @@ public class PyDirectoryIconProvider extends IconProvider { if (element instanceof PsiDirectory) { final PsiDirectory directory = (PsiDirectory)element; // Preserve original icons for excluded directories and source roots - if (!isSpecialDirectory(directory) && isImportableOldStylePackage(directory)) { + if (isSpecialDirectory(directory)) return null; + if (PyUtil.isExplicitPackage(directory)) { return PlatformIcons.PACKAGE_ICON; } } @@ -53,18 +54,4 @@ public class PyDirectoryIconProvider extends IconProvider { final Module module = ModuleUtilCore.findModuleForPsiElement(directory); return module == null || PyUtil.getSourceRoots(module).contains(vFile); } - - private static boolean isImportableOldStylePackage(@NotNull PsiDirectory directory) { - final Collection sourceRoots = PyUtil.getSourceRoots(directory); - for (PsiDirectory dir = directory; dir != null; dir = dir.getParentDirectory()) { - if (sourceRoots.contains(dir.getVirtualFile())) { - return true; - } - if (!PyNames.isIdentifier(dir.getName()) || (dir.findFile(PyNames.INIT_DOT_PY) == null && - dir.findFile(PyNames.INIT_DOT_PYI) == null)) { - return false; - } - } - return false; - } } diff --git a/python/src/com/jetbrains/python/actions/CreatePackageAction.java b/python/src/com/jetbrains/python/actions/CreatePackageAction.java index e1ffbbb6037d..28860b692457 100644 --- a/python/src/com/jetbrains/python/actions/CreatePackageAction.java +++ b/python/src/com/jetbrains/python/actions/CreatePackageAction.java @@ -1,36 +1,43 @@ // Copyright 2000-2019 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.actions; -import com.intellij.ide.IdeBundle; +import com.intellij.application.options.RegistryManager; import com.intellij.ide.IdeView; import com.intellij.ide.actions.CreateDirectoryOrPackageHandler; +import com.intellij.ide.actions.CreateFileFromTemplateDialog; import com.intellij.ide.fileTemplates.FileTemplate; import com.intellij.ide.fileTemplates.FileTemplateManager; import com.intellij.ide.fileTemplates.FileTemplateUtil; -import com.intellij.ide.ui.newItemPopup.NewItemPopupUtil; -import com.intellij.ide.ui.newItemPopup.NewItemSimplePopupPanel; import com.intellij.ide.util.DirectoryChooserUtil; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.LangDataKeys; -import com.intellij.openapi.application.Experiments; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.module.Module; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFileSystemItem; import com.intellij.psi.SmartPointerManager; import com.intellij.psi.SmartPsiElementPointer; +import com.intellij.util.EmptyConsumer; +import com.intellij.util.PlatformIcons; +import com.jetbrains.python.PyBundle; import com.jetbrains.python.PyNames; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesStatisticsCollector; +import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import javax.swing.*; import java.util.function.Consumer; public final class CreatePackageAction extends DumbAwareAction { private static final Logger LOG = Logger.getInstance(CreatePackageAction.class); + private static final @NonNls String NAMESPACE_PACKAGE_TYPE = "Namespace Package"; + private static final @NonNls String ORDINARY_PACKAGE_TYPE = "Package"; + @Override public void actionPerformed(@NotNull AnActionEvent e) { @@ -38,13 +45,62 @@ public final class CreatePackageAction extends DumbAwareAction { if (view == null) { return; } - final Project project = e.getData(CommonDataKeys.PROJECT); - final PsiDirectory directory = DirectoryChooserUtil.getOrChooseDirectory(view); + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null) return; + + final PsiDirectory directory = DirectoryChooserUtil.getOrChooseDirectory(view); if (directory == null) return; + + Module module = e.getData(LangDataKeys.MODULE); + if (module == null) return; + + final CreateFileFromTemplateDialog.Builder builder = createDialogBuilder(project); + final SmartPointerManager pointerManager = SmartPointerManager.getInstance(project); final SmartPsiElementPointer directoryPointer = pointerManager.createSmartPsiElementPointer(directory); - final CreateDirectoryOrPackageHandler validator = new CreateDirectoryOrPackageHandler(project, directory, false, ".") { + final CreateDirectoryOrPackageHandler newOrdinaryPackageHandler = getNewOrdinaryPackageHandler(project, directory, directoryPointer); + final CreateDirectoryOrPackageHandler newNamespacePackageHandler = getNewNamespacePackageHandler(project, directory, directoryPointer, module); + + builder.show(PyBundle.message("dialog.title.can.t.create.package"), ORDINARY_PACKAGE_TYPE, + new CreateFileFromTemplateDialog.FileCreator() { + @Override + public PsiDirectory createFile(@NotNull String name, @NotNull String templateName) { + if (templateName.equals(ORDINARY_PACKAGE_TYPE)) { + createNewPackage(name, newOrdinaryPackageHandler, item -> { + if (item != null) { + view.selectElement(item); + } + }); + } + else if (templateName.equals(NAMESPACE_PACKAGE_TYPE)) { + createNewPackage(name, newNamespacePackageHandler, item -> { + if (item != null) { + view.selectElement(item); + } + }); + PyNamespacePackagesStatisticsCollector.Companion.logNamespacePackageCreatedByUser(); + } + return directory; + } + + @Override + public boolean startInWriteAction() { + return false; + } + + @Override + @NotNull + public String getActionName(@NotNull String name, @NotNull String templateName) { + return PyBundle.message("command.name.create.new.package", name); + } + }, EmptyConsumer.getInstance()); + } + + private static CreateDirectoryOrPackageHandler getNewOrdinaryPackageHandler(@NotNull Project project, + @NotNull PsiDirectory directory, + @NotNull SmartPsiElementPointer directoryPointer) { + return new CreateDirectoryOrPackageHandler(project, directory, false, ".") { @Override protected void createDirectories(String subDirName) { super.createDirectories(subDirName); @@ -55,57 +111,69 @@ public final class CreatePackageAction extends DumbAwareAction { } } }; - - Consumer consumer = item -> { - if (item != null) { - view.selectElement(item); - } - }; - - if (Experiments.getInstance().isFeatureEnabled("show.create.new.element.in.popup")) { - JBPopup popup = createLightWeightPopup(validator, consumer); - if (project != null) { - popup.showCenteredInCurrentWindow(project); - } - else { - popup.showInFocusCenter(); - } - } - else { - Messages.showInputDialog(project, IdeBundle.message("prompt.enter.new.package.name"), IdeBundle.message("title.new.package"), Messages.getQuestionIcon(), "", validator); - consumer.accept(validator.getCreatedElement()); - } - } - public static void createInitPyInHierarchy(PsiDirectory created, PsiDirectory ancestor) { + private static CreateDirectoryOrPackageHandler getNewNamespacePackageHandler(@NotNull Project project, + @NotNull PsiDirectory directory, + @NotNull SmartPsiElementPointer directoryPointer, + @NotNull Module module) { + return new CreateDirectoryOrPackageHandler(project, directory, false, ".") { + @Override + protected void createDirectories(String subDirName) { + super.createDirectories(subDirName); + + final PsiDirectory restoredDirectory = directoryPointer.getElement(); + if (restoredDirectory == null) return; + PsiFileSystemItem element = getCreatedElement(); + PsiFileSystemItem lastElement = element; + while (element != null && !element.equals(restoredDirectory)) { + lastElement = element; + element = element.getParent(); + } + if (element == null) return; + + VirtualFile topmostCreatedDirectory = lastElement.getVirtualFile(); + if (topmostCreatedDirectory == null) return; + PyNamespacePackagesService.getInstance(module).toggleMarkingAsNamespacePackage(topmostCreatedDirectory); + } + }; + } + + private static void createNewPackage(@NotNull String name, + @NotNull CreateDirectoryOrPackageHandler createHandler, + @NotNull Consumer consumer) { + if (createHandler.checkInput(name) && createHandler.canClose(name)) { + consumer.accept(createHandler.getCreatedElement()); + } + else { + String errorMessage = createHandler.getErrorText(name); + Messages.showErrorDialog(errorMessage, PyBundle.message("dialog.title.can.t.create.package")); + } + } + + @SuppressWarnings("TestOnlyProblems") + private static CreateFileFromTemplateDialog.Builder createDialogBuilder(@NotNull Project project) { + CreateFileFromTemplateDialog.Builder builder = CreateFileFromTemplateDialog.createDialog(project); + builder + .setTitle(PyBundle.message("dialog.title.new.python.package")) + .addKind(PyBundle.message("new.package.list.item.ordinary.package"), PlatformIcons.PACKAGE_ICON, ORDINARY_PACKAGE_TYPE); + + if (PyNamespacePackagesService.isEnabled() && RegistryManager.getInstance().is("python.create.namespace.package.action")) { + builder.addKind(PyBundle.message("new.package.list.item.namespace.package"), PlatformIcons.PACKAGE_ICON, NAMESPACE_PACKAGE_TYPE); + } + + return builder; + } + + + public static void createInitPyInHierarchy(@NotNull PsiDirectory created, @NotNull PsiDirectory ancestor) { do { createInitPy(created); created = created.getParent(); } while(created != null && !created.equals(ancestor)); } - private static JBPopup createLightWeightPopup(CreateDirectoryOrPackageHandler validator, - Consumer consumer) { - NewItemSimplePopupPanel contentPanel = new NewItemSimplePopupPanel(); - JTextField nameField = contentPanel.getTextField(); - JBPopup popup = NewItemPopupUtil.createNewItemPopup(IdeBundle.message("title.new.package"), contentPanel, nameField); - contentPanel.setApplyAction(event -> { - String name = nameField.getText(); - if (validator.checkInput(name) && validator.canClose(name)) { - popup.closeOk(event); - consumer.accept(validator.getCreatedElement()); - } - else { - String errorMessage = validator.getErrorText(name); - contentPanel.setError(errorMessage); - } - }); - - return popup; - } - - private static void createInitPy(PsiDirectory directory) { + private static void createInitPy(@NotNull PsiDirectory directory) { final FileTemplateManager fileTemplateManager = FileTemplateManager.getInstance(directory.getProject()); final FileTemplate template = fileTemplateManager.getInternalTemplate("Python Script"); if (directory.findFile(PyNames.INIT_DOT_PY) != null) { diff --git a/python/src/com/jetbrains/python/inspections/PyRelativeImportInspection.kt b/python/src/com/jetbrains/python/inspections/PyRelativeImportInspection.kt new file mode 100644 index 000000000000..6ec812052905 --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyRelativeImportInspection.kt @@ -0,0 +1,132 @@ +// Copyright 2000-2020 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.inspections + +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.command.undo.BasicUndoableAction +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.PyBundle +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher + +class PyRelativeImportInspection : PyInspection() { + override fun buildVisitor(holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession): PsiElementVisitor { + if (!PyNamespacePackagesService.isEnabled() || + LanguageLevel.forElement(holder.file).isOlderThan(LanguageLevel.PYTHON34)) { + return PsiElementVisitor.EMPTY_VISITOR + } + return Visitor(holder, session) + } + + private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) { + override fun visitPyFromImportStatement(node: PyFromImportStatement) { + val directory = node.containingFile?.containingDirectory ?: return + if (node.relativeLevel > 0 && !PyUtil.isExplicitPackage(directory) && !isInsideOrdinaryPackage(directory)) { + handleRelativeImportNotInsidePackage(node, directory) + } + } + + private fun isInsideOrdinaryPackage(directory: PsiDirectory): Boolean { + var curDir: PsiDirectory? = directory + while (curDir != null) { + if (PyUtil.isOrdinaryPackage(curDir)) return true + curDir = curDir.parentDirectory + } + return false + } + + private fun handleRelativeImportNotInsidePackage(node: PyFromImportStatement, directory: PsiDirectory) { + val fixes = mutableListOf() + getMarkAsNamespacePackageQuickFix(directory) ?.let { fixes.add(it) } + if (node.relativeLevel == 1) { + fixes.add(PyChangeToSameDirectoryImportQuickFix()) + } + val message = PyPsiBundle.message("INSP.relative.import.relative.import.outside.package") + registerProblem(node, message, *fixes.toTypedArray()) + } + + private fun getMarkAsNamespacePackageQuickFix(directory: PsiDirectory): PyMarkAsNamespacePackageQuickFix? { + val module = ModuleUtilCore.findModuleForPsiElement(directory) ?: return null + + var curDir: PsiDirectory? = directory + while (curDir != null) { + val virtualFile = curDir.virtualFile + if (PyUtil.isRoot(curDir)) return null + + val parentDir = curDir.parentDirectory + if (parentDir != null && (PyUtil.isRoot(parentDir) || PyUtil.isOrdinaryPackage(parentDir))) { + return PyMarkAsNamespacePackageQuickFix(module, virtualFile) + } + + curDir = parentDir + } + + return null + } + } + + private class PyMarkAsNamespacePackageQuickFix(val module: Module, val directory: VirtualFile) : LocalQuickFix { + override fun getFamilyName(): String = PyBundle.message("QFIX.mark.as.namespace.package", directory.name) + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val document = PsiDocumentManager.getInstance(project).getDocument(descriptor.psiElement.containingFile) + val undoableAction = object: BasicUndoableAction(document) { + override fun undo() { + PyNamespacePackagesService.getInstance(module).toggleMarkingAsNamespacePackage(directory) + } + + override fun redo() { + PyNamespacePackagesService.getInstance(module).toggleMarkingAsNamespacePackage(directory) + } + } + undoableAction.redo() + UndoManager.getInstance(project).undoableActionPerformed(undoableAction) + } + } + + private class PyChangeToSameDirectoryImportQuickFix : LocalQuickFix { + override fun getFamilyName(): String = PyBundle.message("QFIX.change.to.same.directory.import") + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val oldImport = descriptor.psiElement as? PyFromImportStatement ?: return + assert(oldImport.relativeLevel == 1) + val qualifier = oldImport.importSource + if (qualifier != null) { + val possibleDot = PsiTreeUtil.prevVisibleLeaf(qualifier) + assert(possibleDot != null && possibleDot.node.elementType == PyTokenTypes.DOT) + possibleDot?.delete() + } + else { + replaceByImportStatements(oldImport) + } + } + + private fun replaceByImportStatements(oldImport: PyFromImportStatement) { + val project = oldImport.project + val generator = PyElementGenerator.getInstance(project) + val names = oldImport.importElements.map { it.text } + if (names.isEmpty()) return + val langLevel = LanguageLevel.forElement(oldImport) + for (name in names.reversed()) { + val newImport = generator.createImportStatement(langLevel, name, null) + oldImport.parent.addAfter(newImport, oldImport) + } + oldImport.delete() + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/module/PyContentEntriesEditor.java b/python/src/com/jetbrains/python/module/PyContentEntriesEditor.java index 02ad56880453..192d45b1ae0e 100644 --- a/python/src/com/jetbrains/python/module/PyContentEntriesEditor.java +++ b/python/src/com/jetbrains/python/module/PyContentEntriesEditor.java @@ -16,6 +16,7 @@ import com.intellij.openapi.util.Disposer; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.pointers.VirtualFilePointer; import com.intellij.util.EventDispatcher; +import com.intellij.util.PlatformIcons; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import org.jetbrains.annotations.NotNull; @@ -30,6 +31,7 @@ import java.awt.*; import java.util.Collection; import java.util.List; import java.util.Objects; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; public class PyContentEntriesEditor extends CommonContentEntriesEditor { private final List myRootTypeProviders; @@ -280,7 +282,14 @@ public class PyContentEntriesEditor extends CommonContentEntriesEditor { return provider.getIcon(); } } - return super.updateIcon(entry, file, originalIcon); + // JavaModuleSourceRootEditHandler gives every directory under a source root a package icon. + // Since we use the same icon for explicit namespace package "roots", we forcibly replace icons + // for other "false" packages with the one for a plain directory to avoid confusion. + Icon defaultIcon = super.updateIcon(entry, file, originalIcon); + if (defaultIcon == PlatformIcons.PACKAGE_ICON) { + return PlatformIcons.FOLDER_ICON; + } + return defaultIcon; } }; } diff --git a/python/src/com/jetbrains/python/namespacePackages/PyMarkAsNamespacePackageAction.kt b/python/src/com/jetbrains/python/namespacePackages/PyMarkAsNamespacePackageAction.kt new file mode 100644 index 000000000000..e64319e4d346 --- /dev/null +++ b/python/src/com/jetbrains/python/namespacePackages/PyMarkAsNamespacePackageAction.kt @@ -0,0 +1,47 @@ +// Copyright 2000-2020 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.namespacePackages + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.LangDataKeys +import com.intellij.util.PlatformIcons +import com.jetbrains.python.PyBundle + +class PyMarkAsNamespacePackageAction : AnAction() { + override fun update(e: AnActionEvent) { + val presentation = e.presentation + presentation.isEnabled = false + presentation.isVisible = false + + val virtualFiles = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) ?: return + if (virtualFiles.isEmpty()) return + + val module = e.getData(LangDataKeys.MODULE) ?: return + val service = PyNamespacePackagesService.getInstance(module) + if (!PyNamespacePackagesService.isEnabled()) return + presentation.isVisible = true + + presentation.icon = PlatformIcons.PACKAGE_ICON + when { + virtualFiles.all { service.canBeMarked(it) } -> { + presentation.isEnabled = true + presentation.text = PyBundle.message("python.namespace.package.folder") + } + virtualFiles.all { service.isMarked(it) } -> { + presentation.isEnabled = true + presentation.text = PyBundle.message("python.unmark.as.namespace.package") + } + else -> { + presentation.isEnabled = false + } + } + } + + override fun actionPerformed(e: AnActionEvent) { + val module = e.getData(LangDataKeys.MODULE) ?: return + val virtualFiles = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) ?: return + val service = PyNamespacePackagesService.getInstance(module) + virtualFiles.forEach { service.toggleMarkingAsNamespacePackage(it) } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackageCustomIdentifier.kt b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackageCustomIdentifier.kt new file mode 100644 index 000000000000..bb08b89c7583 --- /dev/null +++ b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackageCustomIdentifier.kt @@ -0,0 +1,26 @@ +// Copyright 2000-2020 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.namespacePackages + +import com.intellij.openapi.extensions.ExtensionNotApplicableException +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.util.registry.Registry +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiFile +import com.jetbrains.python.psi.PyCustomPackageIdentifier + +class PyNamespacePackageCustomIdentifier : PyCustomPackageIdentifier { + init { + if (!Registry.`is`("python.explicit.namespace.packages")) { + throw ExtensionNotApplicableException.INSTANCE + } + } + + override fun isPackage(directory: PsiDirectory?): Boolean { + if (directory == null) return false + if (!Registry.`is`("python.explicit.namespace.packages")) return false + val module = ModuleUtilCore.findModuleForPsiElement(directory) ?: return false + return PyNamespacePackagesService.getInstance(module).isNamespacePackage(directory.virtualFile) + } + + override fun isPackageFile(file: PsiFile): Boolean = false +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackageRootProvider.kt b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackageRootProvider.kt new file mode 100644 index 000000000000..cbc801656c3c --- /dev/null +++ b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackageRootProvider.kt @@ -0,0 +1,95 @@ +// Copyright 2000-2020 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.namespacePackages + +import com.intellij.openapi.Disposable +import com.intellij.openapi.extensions.ExtensionNotApplicableException +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.ContentEntry +import com.intellij.openapi.roots.ModifiableRootModel +import com.intellij.openapi.roots.ui.configuration.actions.ContentEntryEditingAction +import com.intellij.openapi.util.Comparing +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.pointers.VirtualFilePointer +import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager +import com.intellij.ui.JBColor +import com.intellij.util.PlatformIcons +import com.intellij.util.containers.MultiMap +import com.jetbrains.python.PyBundle +import com.jetbrains.python.module.PyContentEntriesEditor +import com.jetbrains.python.module.PyRootTypeProvider +import java.awt.Color +import javax.swing.Icon +import javax.swing.JTree + +class PyNamespacePackageRootProvider: PyRootTypeProvider() { + private val myNamespacePackages = MultiMap() + + init { + if (!Registry.`is`("python.explicit.namespace.packages")) { + throw ExtensionNotApplicableException.INSTANCE + } + } + + override fun reset(disposable: Disposable, editor: PyContentEntriesEditor, module: Module) { + myNamespacePackages.clear() + val namespacePackages = PyNamespacePackagesService.getInstance(module).namespacePackageFoldersVirtualFiles + for (namespacePackage in namespacePackages) { + val contentEntry = findContentEntryForFile(namespacePackage, editor) ?: continue + val pointer = VirtualFilePointerManager.getInstance().create(namespacePackage, disposable, DUMMY_LISTENER) + myNamespacePackages.putValue(contentEntry, pointer) + } + } + + override fun apply(module: Module) { + val instance = PyNamespacePackagesService.getInstance(module) + val currentNamespacePackages = getCurrentNamespacePackages() + if (!Comparing.haveEqualElements(instance.namespacePackageFoldersVirtualFiles, currentNamespacePackages)) { + instance.namespacePackageFoldersVirtualFiles = currentNamespacePackages + PyNamespacePackagesStatisticsCollector.logApplyInNamespacePackageRootProvider() + } + } + + override fun isModified(module: Module): Boolean = + !Comparing.haveEqualElements(PyNamespacePackagesService.getInstance(module).namespacePackageFoldersVirtualFiles, + getCurrentNamespacePackages()) + + override fun getRoots(): MultiMap = myNamespacePackages + + override fun getIcon(): Icon { + return PlatformIcons.PACKAGE_ICON + } + + override fun getName(): String { + return PyBundle.message("python.namespace.packages.name") + } + + override fun getDescription(): String { + return PyBundle.message("python.namespace.packages.description") + } + + override fun getColor(): Color { + return EASTERN_BLUE + } + + override fun createRootEntryEditingAction(tree: JTree?, + disposable: Disposable?, + editor: PyContentEntriesEditor?, + model: ModifiableRootModel?): ContentEntryEditingAction { + return RootEntryEditingAction(tree, disposable, editor, model) + } + + private fun getCurrentNamespacePackages(): List = myNamespacePackages.values().mapNotNull { it.file } + + companion object { + private fun findContentEntryForFile(virtualFile: VirtualFile, editor: PyContentEntriesEditor): ContentEntry? { + return editor.contentEntries.find { + val possibleContentEntry = it.file + possibleContentEntry != null && VfsUtilCore.isAncestor(possibleContentEntry, virtualFile, false) + } + } + + private val EASTERN_BLUE: Color = JBColor(0x29A5AD, 0x29A5AD) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackagesService.java b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackagesService.java new file mode 100644 index 000000000000..7e9d9538e3bb --- /dev/null +++ b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackagesService.java @@ -0,0 +1,177 @@ +// Copyright 2000-2020 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.namespacePackages; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.ide.projectView.ProjectView; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiManager; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xmlb.XmlSerializerUtil; +import com.intellij.util.xmlb.annotations.Transient; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.PyUtil; +import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@State(name = "PyNamespacePackagesService") +public class PyNamespacePackagesService implements PersistentStateComponent { + private final List myNamespacePackageFolders = new ArrayList<>(); + private final Module myModule; + + public PyNamespacePackagesService() { + myModule = null; + } + + public PyNamespacePackagesService(@Nullable Module module) { + myModule = module; + } + + public static @NotNull PyNamespacePackagesService getInstance(@NotNull Module module) { + return module.getService(PyNamespacePackagesService.class); + } + + public @NotNull List getNamespacePackageFolders() { + removeInvalidNamespacePackageFolders(); + return Collections.unmodifiableList(ContainerUtil.map(myNamespacePackageFolders, it -> it.getPath())); + } + + @Transient + public @NotNull List getNamespacePackageFoldersVirtualFiles() { + removeInvalidNamespacePackageFolders(); + return Collections.unmodifiableList(myNamespacePackageFolders); + } + + public void setNamespacePackageFolders(@NotNull List folders) { + myNamespacePackageFolders.clear(); + for (String path: folders) { + VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(path); + if (virtualFile != null) { + myNamespacePackageFolders.add(virtualFile); + } + } + } + + @Transient + public void setNamespacePackageFoldersVirtualFiles(@NotNull List folders) { + myNamespacePackageFolders.clear(); + myNamespacePackageFolders.addAll(folders); + } + + public void toggleMarkingAsNamespacePackage(@NotNull VirtualFile directory) { + if (!directory.isDirectory()) return; + + if (canBeMarked(directory)) { + myNamespacePackageFolders.add(directory); + PyNamespacePackagesStatisticsCollector.Companion.logToggleMarkingAsNamespacePackage(true); + } + else if (isMarked(directory)) { + myNamespacePackageFolders.remove(directory); + PyNamespacePackagesStatisticsCollector.Companion.logToggleMarkingAsNamespacePackage(false); + } + else { + throw new IllegalStateException("Can't toggle namespace package state for: " + directory.getName()); + } + + refreshView(); + } + + public boolean canBeMarked(@NotNull VirtualFile virtualFile) { + if (myModule == null) return false; + Project project = myModule.getProject(); + + if (PythonLanguageLevelPusher.getLanguageLevelForVirtualFile(project, virtualFile).isOlderThan(LanguageLevel.PYTHON34)) return false; + if (PyUtil.isRoot(virtualFile, project)) return false; + if (!isInProject(virtualFile, project)) return false; + PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(virtualFile); + if (psiDirectory != null && PyUtil.isOrdinaryPackage(psiDirectory)) return false; + + VirtualFile curDir = virtualFile; + while (curDir != null) { + if (!curDir.isDirectory()) return false; + if (myNamespacePackageFolders.contains(curDir)) return false; + if (PyUtil.isRoot(curDir, project)) break; + psiDirectory = PsiManager.getInstance(myModule.getProject()).findDirectory(curDir); + if (psiDirectory != null && PyUtil.isOrdinaryPackage(psiDirectory)) break; + curDir = curDir.getParent(); + } + + return true; + } + + @Nullable + @Override + public PyNamespacePackagesService getState() { + return this; + } + + @Override + public void loadState(@NotNull PyNamespacePackagesService state) { + XmlSerializerUtil.copyBean(state, this); + } + + public boolean isMarked(@NotNull VirtualFile dir) { + return myNamespacePackageFolders.contains(dir); + } + + public boolean isNamespacePackage(VirtualFile directory) { + if (myModule != null) { + PsiDirectory psiDirectory = PsiManager.getInstance(myModule.getProject()).findDirectory(directory); + if (psiDirectory != null && PyUtil.isOrdinaryPackage(psiDirectory)) return false; + } + VirtualFile curDir = directory; + while (curDir != null) { + if (isMarked(curDir)) return true; + if (myModule != null) { + if (PyUtil.isRoot(directory, myModule.getProject())) break; + PsiDirectory psiDirectory = PsiManager.getInstance(myModule.getProject()).findDirectory(curDir); + if (psiDirectory != null && PyUtil.isOrdinaryPackage(psiDirectory)) break; + } + curDir = curDir.getParent(); + } + return false; + } + + public static boolean isEnabled() { + return Registry.is("python.explicit.namespace.packages"); + } + + private void removeInvalidNamespacePackageFolders() { + myNamespacePackageFolders.removeIf(it -> it == null || !it.isValid()); + } + + private static boolean isInProject(@NotNull VirtualFile virtualFile, @NotNull Project project) { + PsiManager psiManager = PsiManager.getInstance(project); + PsiDirectory psiDirectory = psiManager.findDirectory(virtualFile); + if (psiDirectory == null) return false; + if (!psiManager.isInProject(psiDirectory)) return false; + return true; + } + + private void refreshView() { + if (!ApplicationManager.getApplication().isWriteThread()) return; + if (myModule == null) return; + Project project = myModule.getProject(); + ProjectView.getInstance(project).refresh(); + PsiManager.getInstance(project).dropPsiCaches(); + DaemonCodeAnalyzer.getInstance(project).restart(); + } + + @TestOnly + public void resetAllNamespacePackages() { + myNamespacePackageFolders.clear(); + } +} diff --git a/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackagesStatisticsCollector.kt b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackagesStatisticsCollector.kt new file mode 100644 index 000000000000..8c70c67c4e54 --- /dev/null +++ b/python/src/com/jetbrains/python/namespacePackages/PyNamespacePackagesStatisticsCollector.kt @@ -0,0 +1,32 @@ +// Copyright 2000-2020 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.namespacePackages + +import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.events.EventFields +import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector + +class PyNamespacePackagesStatisticsCollector : CounterUsagesCollector() { + override fun getGroup(): EventLogGroup = GROUP + + companion object { + fun logNamespacePackageCreatedByUser() { + namespacePackageCreatedEvent.log() + } + + fun logToggleMarkingAsNamespacePackage(mark: Boolean) { + toggleMarkingAsNamespacePackageEvent.log(mark) + } + + fun logApplyInNamespacePackageRootProvider() { + namespacePackagesEditedViaProjectStructure.log() + } + + private val GROUP = EventLogGroup("python.namespace.packages.events", 1) + + private val namespacePackageCreatedEvent = GROUP.registerEvent("namespace.package.created") + + private val toggleMarkingAsNamespacePackageEvent = GROUP.registerEvent("namespace.package.mark.or.unmark", EventFields.Boolean("is_mark")) + + private val namespacePackagesEditedViaProjectStructure = GROUP.registerEvent("namespace.package.apply.in.root.provider") + } +} \ No newline at end of file diff --git a/python/testData/actions/MarkAsNamespacePackage/NestedNamespacePackage/namespacePackage/nestedNamespacePackage/mod.py b/python/testData/actions/MarkAsNamespacePackage/NestedNamespacePackage/namespacePackage/nestedNamespacePackage/mod.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/actions/MarkAsNamespacePackage/OrdinaryPackage/ordinaryPackage/__init__.py b/python/testData/actions/MarkAsNamespacePackage/OrdinaryPackage/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/actions/MarkAsNamespacePackage/PlainDirectory/plainDirectory/script.py b/python/testData/actions/MarkAsNamespacePackage/PlainDirectory/plainDirectory/script.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/actions/MarkAsNamespacePackage/PlainDirectoryInOrdinaryPackage/ordinaryPackage/__init__.py b/python/testData/actions/MarkAsNamespacePackage/PlainDirectoryInOrdinaryPackage/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/actions/MarkAsNamespacePackage/PlainDirectoryInOrdinaryPackage/ordinaryPackage/nestedPlainDirectory/script.py b/python/testData/actions/MarkAsNamespacePackage/PlainDirectoryInOrdinaryPackage/ordinaryPackage/nestedPlainDirectory/script.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyArgumentListInspection/ImportedTypedDict/a.py b/python/testData/inspections/PyArgumentListInspection/ImportedTypedDict/a.py index 2246c390a5dc..dd0097e8a493 100644 --- a/python/testData/inspections/PyArgumentListInspection/ImportedTypedDict/a.py +++ b/python/testData/inspections/PyArgumentListInspection/ImportedTypedDict/a.py @@ -1,4 +1,4 @@ -from .test import Test1, Base +from test import Test1, Base b = Base(a=2) t = Test1(a=2) diff --git a/python/testData/inspections/PyRelativeImportInspection/NamespacePackageDottedImportNoInspection/namespacePackage/mod.py b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageDottedImportNoInspection/namespacePackage/mod.py new file mode 100644 index 000000000000..845bf4d4daab --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageDottedImportNoInspection/namespacePackage/mod.py @@ -0,0 +1,2 @@ +from util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NamespacePackageDottedImportNoInspection/namespacePackage/util.py b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageDottedImportNoInspection/namespacePackage/util.py new file mode 100644 index 000000000000..b8df4015bef1 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageDottedImportNoInspection/namespacePackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print("ok") \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportNoInspection/namespacePackage/mod.py b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportNoInspection/namespacePackage/mod.py new file mode 100644 index 000000000000..94533a60018b --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportNoInspection/namespacePackage/mod.py @@ -0,0 +1,2 @@ +from .util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportNoInspection/namespacePackage/util.py b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportNoInspection/namespacePackage/util.py new file mode 100644 index 000000000000..b8df4015bef1 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportNoInspection/namespacePackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print("ok") \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/mod.py b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/mod.py new file mode 100644 index 000000000000..845bf4d4daab --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/mod.py @@ -0,0 +1,2 @@ +from util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/util.py b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/util.py new file mode 100644 index 000000000000..b8df4015bef1 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print("ok") \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NestedNamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/nestedNamespacePackage/mod.py b/python/testData/inspections/PyRelativeImportInspection/NestedNamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/nestedNamespacePackage/mod.py new file mode 100644 index 000000000000..845bf4d4daab --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NestedNamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/nestedNamespacePackage/mod.py @@ -0,0 +1,2 @@ +from util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NestedNamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/nestedNamespacePackage/util.py b/python/testData/inspections/PyRelativeImportInspection/NestedNamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/nestedNamespacePackage/util.py new file mode 100644 index 000000000000..b8df4015bef1 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NestedNamespacePackageSameDirectoryImportRegistryOffNoInspection/namespacePackage/nestedNamespacePackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print("ok") \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/dottedImport.py b/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/dottedImport.py new file mode 100644 index 000000000000..e9a2ed34bf74 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/dottedImport.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/dottedImport_after.py b/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/dottedImport_after.py new file mode 100644 index 000000000000..f3fa9f3e05f9 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/dottedImport_after.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NestedPlainDirectoryDottedImportMarkDirectory/plainDirectory/nestedPlainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/__init__.py b/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/nestedNamespacePackage/mod.py b/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/nestedNamespacePackage/mod.py new file mode 100644 index 000000000000..845bf4d4daab --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/nestedNamespacePackage/mod.py @@ -0,0 +1,2 @@ +from util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/nestedNamespacePackage/util.py b/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/nestedNamespacePackage/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/NotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection/namespacePackage/ordinaryPackage/nestedNamespacePackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/__init__.py b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/script.py b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/script.py new file mode 100644 index 000000000000..f3fa9f3e05f9 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/script.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/util.py b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageDottedImportNoInspection/ordinaryPackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/__init__.py b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/script.py b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/script.py new file mode 100644 index 000000000000..1882243a642a --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/script.py @@ -0,0 +1 @@ +from util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/util.py b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/OrdinaryPackageSameDirectoryImportNoInspection/ordinaryPackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/dottedImport.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/dottedImport.py new file mode 100644 index 000000000000..e9a2ed34bf74 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/dottedImport.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/dottedImport_after.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/dottedImport_after.py new file mode 100644 index 000000000000..1882243a642a --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/dottedImport_after.py @@ -0,0 +1 @@ +from util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportDeleteDot/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/dottedImport.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/dottedImport.py new file mode 100644 index 000000000000..e9a2ed34bf74 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/dottedImport.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/dottedImport_after.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/dottedImport_after.py new file mode 100644 index 000000000000..f3fa9f3e05f9 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/dottedImport_after.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportMarkDirectory/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportRegistryOffNoInspection/plainDirectory/dottedImport.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportRegistryOffNoInspection/plainDirectory/dottedImport.py new file mode 100644 index 000000000000..f3fa9f3e05f9 --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportRegistryOffNoInspection/plainDirectory/dottedImport.py @@ -0,0 +1 @@ +from .util import foo \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportRegistryOffNoInspection/plainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportRegistryOffNoInspection/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryDottedImportRegistryOffNoInspection/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/__init__.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/nestedPlainDirectory/dottedImport.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/nestedPlainDirectory/dottedImport.py new file mode 100644 index 000000000000..94533a60018b --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/nestedPlainDirectory/dottedImport.py @@ -0,0 +1,2 @@ +from .util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/nestedPlainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/nestedPlainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/PlainDirectoryInsidePackageInsidePlainDirectoryNoInspection/plainDirectory/ordinaryPackage/nestedPlainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/Python2PlainDirectoryNoInspection/plainDirectory/script.py b/python/testData/inspections/PyRelativeImportInspection/Python2PlainDirectoryNoInspection/plainDirectory/script.py new file mode 100644 index 000000000000..94533a60018b --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/Python2PlainDirectoryNoInspection/plainDirectory/script.py @@ -0,0 +1,2 @@ +from .util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/Python2PlainDirectoryNoInspection/plainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/Python2PlainDirectoryNoInspection/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/Python2PlainDirectoryNoInspection/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/SourceRootDottedImportInspectionWithoutQuickFixes/plainDirectory/sourceRoot/script.py b/python/testData/inspections/PyRelativeImportInspection/SourceRootDottedImportInspectionWithoutQuickFixes/plainDirectory/sourceRoot/script.py new file mode 100644 index 000000000000..0b906a32a1fd --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/SourceRootDottedImportInspectionWithoutQuickFixes/plainDirectory/sourceRoot/script.py @@ -0,0 +1,2 @@ +from ..util import foo +foo() \ No newline at end of file diff --git a/python/testData/inspections/PyRelativeImportInspection/SourceRootDottedImportInspectionWithoutQuickFixes/plainDirectory/util.py b/python/testData/inspections/PyRelativeImportInspection/SourceRootDottedImportInspectionWithoutQuickFixes/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/inspections/PyRelativeImportInspection/SourceRootDottedImportInspectionWithoutQuickFixes/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/inspections/PyTypedDictInspection/ImportedTypedDict/a.py b/python/testData/inspections/PyTypedDictInspection/ImportedTypedDict/a.py index 0504b759d7fb..c69369b14172 100644 --- a/python/testData/inspections/PyTypedDictInspection/ImportedTypedDict/a.py +++ b/python/testData/inspections/PyTypedDictInspection/ImportedTypedDict/a.py @@ -1,4 +1,4 @@ -from .test import Test1, Base +from test import Test1, Base b = Base(a=2) t = Test1(a=2) diff --git a/python/testData/packageIcon/DirectoryInOrdinaryPackageInNamespacePackage/nspkg/pkg/__init__.py b/python/testData/packageIcon/DirectoryInOrdinaryPackageInNamespacePackage/nspkg/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/DirectoryInOrdinaryPackageInNamespacePackage/nspkg/pkg/directory/.keep b/python/testData/packageIcon/DirectoryInOrdinaryPackageInNamespacePackage/nspkg/pkg/directory/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/NamespacePackageInsideOldStylePackage/pkg/__init__.py b/python/testData/packageIcon/NamespacePackageInsideOldStylePackage/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/NamespacePackageInsideOldStylePackage/pkg/nspkg/.keep b/python/testData/packageIcon/NamespacePackageInsideOldStylePackage/pkg/nspkg/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/NamespacePackageInsideOldStylePackageRegistryOff/pkg/__init__.py b/python/testData/packageIcon/NamespacePackageInsideOldStylePackageRegistryOff/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/NamespacePackageInsideOldStylePackageRegistryOff/pkg/nspkg/.keep b/python/testData/packageIcon/NamespacePackageInsideOldStylePackageRegistryOff/pkg/nspkg/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/NamespacePackageRegistryOff/nspkg/subpkg/.keep b/python/testData/packageIcon/NamespacePackageRegistryOff/nspkg/subpkg/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/OldStylePackageInsideNamespacePackageRegistryOff/nspkg/pkg/__init__.py b/python/testData/packageIcon/OldStylePackageInsideNamespacePackageRegistryOff/nspkg/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/OldStylePackageRegistryOff/pkg/__init__.py b/python/testData/packageIcon/OldStylePackageRegistryOff/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/OldStylePackageRegistryOff/pkg/subpkg/__init__.py b/python/testData/packageIcon/OldStylePackageRegistryOff/pkg/subpkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/PlainDirectoryInsideOldStylePackage/pkg/__init__.py b/python/testData/packageIcon/PlainDirectoryInsideOldStylePackage/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/packageIcon/PlainDirectoryInsideOldStylePackage/pkg/plainDirectory/.keep b/python/testData/packageIcon/PlainDirectoryInsideOldStylePackage/pkg/plainDirectory/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageDottedRelativeImport/namespacePackage/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageDottedRelativeImport/namespacePackage/mod.py new file mode 100644 index 000000000000..ea1b04396bf9 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageDottedRelativeImport/namespacePackage/mod.py @@ -0,0 +1,2 @@ +from . import util +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageDottedRelativeImport/namespacePackage/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageDottedRelativeImport/namespacePackage/util.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageSameDirectoryImport/namespacePackage/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageSameDirectoryImport/namespacePackage/mod.py new file mode 100644 index 000000000000..2862c8ddd788 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageSameDirectoryImport/namespacePackage/mod.py @@ -0,0 +1,2 @@ +import util +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageSameDirectoryImport/namespacePackage/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/namespacePackageSameDirectoryImport/namespacePackage/util.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageDottedRelativeImport/namespacePackage/nestedNamespacePackage/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageDottedRelativeImport/namespacePackage/nestedNamespacePackage/mod.py new file mode 100644 index 000000000000..ea1b04396bf9 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageDottedRelativeImport/namespacePackage/nestedNamespacePackage/mod.py @@ -0,0 +1,2 @@ +from . import util +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageDottedRelativeImport/namespacePackage/nestedNamespacePackage/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageDottedRelativeImport/namespacePackage/nestedNamespacePackage/util.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageSameDirectoryImport/namespacePackage/nestedNamespacePackage/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageSameDirectoryImport/namespacePackage/nestedNamespacePackage/mod.py new file mode 100644 index 000000000000..2862c8ddd788 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageSameDirectoryImport/namespacePackage/nestedNamespacePackage/mod.py @@ -0,0 +1,2 @@ +import util +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageSameDirectoryImport/namespacePackage/nestedNamespacePackage/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/nestedNamespacePackageSameDirectoryImport/namespacePackage/nestedNamespacePackage/util.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/__init__.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/mod.py new file mode 100644 index 000000000000..32fb0f8c00f4 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/mod.py @@ -0,0 +1,2 @@ +from .util import foo +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageDottedRelativeFromImport/ordinaryPackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/__init__.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/mod.py new file mode 100644 index 000000000000..fe1e2830ee2b --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/mod.py @@ -0,0 +1,2 @@ +from util import foo +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/ordinaryPackageSameDirectoryFromImport/ordinaryPackage/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImport/plainDirectory/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImport/plainDirectory/mod.py new file mode 100644 index 000000000000..32fb0f8c00f4 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImport/plainDirectory/mod.py @@ -0,0 +1,2 @@ +from .util import foo +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImport/plainDirectory/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImport/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImport/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImportRegistryOff/plainDirectory/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImportRegistryOff/plainDirectory/mod.py new file mode 100644 index 000000000000..32fb0f8c00f4 --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImportRegistryOff/plainDirectory/mod.py @@ -0,0 +1,2 @@ +from .util import foo +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImportRegistryOff/plainDirectory/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImportRegistryOff/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectoryDottedRelativeImportRegistryOff/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImport/plainDirectory/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImport/plainDirectory/mod.py new file mode 100644 index 000000000000..fe1e2830ee2b --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImport/plainDirectory/mod.py @@ -0,0 +1,2 @@ +from util import foo +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImport/plainDirectory/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImport/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImport/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImportRegistryOff/plainDirectory/mod.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImportRegistryOff/plainDirectory/mod.py new file mode 100644 index 000000000000..fe1e2830ee2b --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImportRegistryOff/plainDirectory/mod.py @@ -0,0 +1,2 @@ +from util import foo +# \ No newline at end of file diff --git a/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImportRegistryOff/plainDirectory/util.py b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImportRegistryOff/plainDirectory/util.py new file mode 100644 index 000000000000..cb332504aa2e --- /dev/null +++ b/python/testData/resolve/multiFile/relativeAndSameDirectoryImports/plainDirectorySameDirectoryImportRegistryOff/plainDirectory/util.py @@ -0,0 +1,2 @@ +def foo(): + print('ok') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyPackageIconTest.java b/python/testSrc/com/jetbrains/python/PyPackageIconTest.java index be32cfc6bb84..aa34f001bc7e 100644 --- a/python/testSrc/com/jetbrains/python/PyPackageIconTest.java +++ b/python/testSrc/com/jetbrains/python/PyPackageIconTest.java @@ -1,6 +1,7 @@ // Copyright 2000-2019 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.application.options.RegistryManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiManager; @@ -8,6 +9,7 @@ import com.intellij.testFramework.LightProjectDescriptor; import com.intellij.util.PlatformIcons; import com.intellij.util.PsiIconUtil; import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,27 +28,75 @@ public class PyPackageIconTest extends PyTestCase { public void setUp() throws Exception { super.setUp(); myFixture.copyDirectoryToProject(getTestName(false), ""); + RegistryManager.getInstance().get("python.explicit.namespace.packages").resetToDefault(); + } + + @Override + public void tearDown() throws Exception { + RegistryManager.getInstance().get("python.explicit.namespace.packages").resetToDefault(); + super.tearDown(); } // PY-38642 + // upd: PY-42750 public void testNamespacePackage() { + toggleNamespacePackageDirectory("nspkg"); + checkPackageIcon("nspkg", true); + checkPackageIcon("nspkg/subpkg", true); + } + + public void testNamespacePackageRegistryOff() { + RegistryManager.getInstance().get("python.explicit.namespace.packages").setValue(false); + toggleNamespacePackageDirectory("nspkg"); checkPackageIcon("nspkg", false); checkPackageIcon("nspkg/subpkg", false); } + public void testPlainDirectoryInsideOldStylePackage() { + checkPackageIcon("pkg", true); + checkPackageIcon("pkg/plainDirectory", false); + } + + public void testNamespacePackageInsideOldStylePackage() { + toggleNamespacePackageDirectory("pkg/nspkg"); + checkPackageIcon("pkg", true); + checkPackageIcon("pkg/nspkg", true); + } + + public void testNamespacePackageInsideOldStylePackageRegistryOff() { + RegistryManager.getInstance().get("python.explicit.namespace.packages").setValue(false); + toggleNamespacePackageDirectory("pkg/nspkg"); + checkPackageIcon("pkg", true); + checkPackageIcon("pkg/nspkg", false); + } + public void testOldStylePackage() { checkPackageIcon("pkg", true); checkPackageIcon("pkg/subpkg", true); } + public void testOldStylePackageRegistryOff() { + RegistryManager.getInstance().get("python.explicit.namespace.packages").setValue(false); + checkPackageIcon("pkg", true); + checkPackageIcon("pkg/subpkg", true); + } + public void testOldStylePackageWithIllegalName() { - checkPackageIcon("illegally named", false); - checkPackageIcon(".pkg", false); + checkPackageIcon("illegally named", true); + checkPackageIcon(".pkg", true); } public void testOldStylePackageInsideNamespacePackage() { + toggleNamespacePackageDirectory("nspkg"); + checkPackageIcon("nspkg", true); + checkPackageIcon("nspkg/pkg", true); + } + + public void testOldStylePackageInsideNamespacePackageRegistryOff() { + RegistryManager.getInstance().get("python.explicit.namespace.packages").setValue(false); + toggleNamespacePackageDirectory("nspkg"); checkPackageIcon("nspkg", false); - checkPackageIcon("nspkg/pkg", false); + checkPackageIcon("nspkg/pkg", true); } public void testOldStylePackageInsideSourceRoot() { @@ -57,6 +107,13 @@ public class PyPackageIconTest extends PyTestCase { }); } + public void testDirectoryInOrdinaryPackageInNamespacePackage() { + toggleNamespacePackageDirectory("nspkg"); + checkPackageIcon("nspkg", true); + checkPackageIcon("nspkg/pkg", true); + checkPackageIcon("nspkg/pkg/directory", false); + } + // PY-39274 public void testStubPackage() { checkPackageIcon("stubpkg", true); @@ -72,6 +129,12 @@ public class PyPackageIconTest extends PyTestCase { assertEquals(PlatformIcons.PACKAGE_ICON.equals(icon), has); } + private void toggleNamespacePackageDirectory(@NotNull String directory) { + PyNamespacePackagesService + .getInstance(myFixture.getModule()) + .toggleMarkingAsNamespacePackage(myFixture.findFileInTempDir(directory)); + } + @Override protected String getTestDataPath() { return super.getTestDataPath() + "/packageIcon"; diff --git a/python/testSrc/com/jetbrains/python/PyRelativeImportResolveTest.java b/python/testSrc/com/jetbrains/python/PyRelativeImportResolveTest.java new file mode 100644 index 000000000000..571d7ec63a5d --- /dev/null +++ b/python/testSrc/com/jetbrains/python/PyRelativeImportResolveTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python; + +import com.intellij.application.options.RegistryManager; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.testFramework.LightProjectDescriptor; +import com.jetbrains.python.fixtures.PyMultiFileResolveTestCase; +import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFunction; +import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class PyRelativeImportResolveTest extends PyMultiFileResolveTestCase { + private static final String PLAIN_DIR = "plainDirectory"; + private static final String NAMESPACE_PACK_DIR = "namespacePackage"; + private static final String ORDINARY_PACK_DIR = "ordinaryPackage"; + + private String myNamespacePackageDirectory = null; + + @Override + protected String getTestDataPath() { + return PythonTestUtil.getTestDataPath() + "/resolve/multiFile/relativeAndSameDirectoryImports/"; + } + + @Override + protected LightProjectDescriptor getProjectDescriptor() { + return PyTestCase.ourPy3Descriptor; + } + + @Override + protected void prepareTestDirectory() { + final String testName = getTestName(true); + myFixture.copyDirectoryToProject(testName, ""); + if (myNamespacePackageDirectory != null) { + toggleNamespacePackageDirectory(myNamespacePackageDirectory); + } + PsiDocumentManager.getInstance(myFixture.getProject()).commitAllDocuments(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myNamespacePackageDirectory = null; + PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), LanguageLevel.getLatest()); + RegistryManager.getInstance().get("python.explicit.namespace.packages").resetToDefault(); + } + + @Override + protected void tearDown() throws Exception { + try { + PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), null); + myNamespacePackageDirectory = null; + } + catch (Throwable e) { + addSuppressedException(e); + } + finally { + RegistryManager.getInstance().get("python.explicit.namespace.packages").resetToDefault(); + super.tearDown(); + } + } + + public void testOrdinaryPackageDottedRelativeFromImport() { + doTestOrdinaryPackageFromImportOfFunction(); + } + + public void testOrdinaryPackageSameDirectoryFromImport() { + doTestOrdinaryPackageFromImportOfFunction(); + } + + private void doTestOrdinaryPackageFromImportOfFunction() { + myTestFileName = ORDINARY_PACK_DIR + "/mod.py"; + assertResolvesTo(PyFunction.class, "foo"); + } + + public void testNamespacePackageDottedRelativeImport() { + doTestNamespacePackageImportOfModule(); + } + + public void testNamespacePackageSameDirectoryImport() { + doTestNamespacePackageImportOfModule(); + } + + private void doTestNamespacePackageImportOfModule() { + myTestFileName = NAMESPACE_PACK_DIR + "/mod.py"; + assertResolvesTo(PyFile.class, "util.py"); + } + + public void testNestedNamespacePackageDottedRelativeImport() { + doTestNestedNamespacePackageImportOfModule(); + } + + public void testNestedNamespacePackageSameDirectoryImport() { + doTestNestedNamespacePackageImportOfModule(); + } + + private void doTestNestedNamespacePackageImportOfModule() { + myTestFileName = NAMESPACE_PACK_DIR + "/nestedNamespacePackage/mod.py"; + myNamespacePackageDirectory = NAMESPACE_PACK_DIR; + assertResolvesTo(PyFile.class, "util.py"); + } + + public void testPlainDirectoryDottedRelativeImport() { + doTestPlainDirectoryImportOfModule(); + } + + public void testPlainDirectorySameDirectoryImport() { + doTestPlainDirectoryImportOfModule(); + } + + private void doTestPlainDirectoryImportOfModule() { + myTestFileName = PLAIN_DIR + "/mod.py"; + assertResolvesTo(PyFunction.class, "foo"); + } + + public void testPlainDirectoryDottedRelativeImportRegistryOff() { + RegistryManager.getInstance().get("python.explicit.namespace.packages").setValue(false); + doTestPlainDirectoryImportOfModule(); + } + + + public void testPlainDirectorySameDirectoryImportRegistryOff() { + RegistryManager.getInstance().get("python.explicit.namespace.packages").setValue(false); + myTestFileName = PLAIN_DIR + "/mod.py"; + assertUnresolved(); + } + + private void toggleNamespacePackageDirectory(@NotNull String directory) { + PyNamespacePackagesService + .getInstance(myFixture.getModule()) + .toggleMarkingAsNamespacePackage(myFixture.findFileInTempDir(directory)); + } +} diff --git a/python/testSrc/com/jetbrains/python/actions/PyMarkAsNamespacePackageActionTest.java b/python/testSrc/com/jetbrains/python/actions/PyMarkAsNamespacePackageActionTest.java new file mode 100644 index 000000000000..b5175449d024 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/actions/PyMarkAsNamespacePackageActionTest.java @@ -0,0 +1,108 @@ +// Copyright 2000-2020 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.actions; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.LangDataKeys; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.MapDataContext; +import com.intellij.testFramework.TestActionEvent; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.PythonTestUtil; +import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.namespacePackages.PyMarkAsNamespacePackageAction; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; +import com.jetbrains.python.psi.LanguageLevel; +import org.jetbrains.annotations.NotNull; + +public class PyMarkAsNamespacePackageActionTest extends PyTestCase { + private static final String PLAIN_DIR = "plainDirectory"; + private static final String NAMESPACE_PACK_DIR = "namespacePackage"; + private static final String ORDINARY_PACK_DIR = "ordinaryPackage"; + + private PyNamespacePackagesService myNspService; + + @Override + protected void setUp() throws Exception { + super.setUp(); + myNspService = PyNamespacePackagesService.getInstance(myFixture.getModule()); + setLanguageLevel(LanguageLevel.getLatest()); + } + + public void testPlainDirectory() { + doCopyDirectory(); + + Presentation presentation = doLaunchAction(PLAIN_DIR); + assertTrue(presentation.isVisible()); + assertTrue(presentation.isEnabled()); + assertEquals(presentation.getText(), PyBundle.message("python.namespace.package.folder")); + assertTrue(myNspService.isMarked(myFixture.findFileInTempDir(PLAIN_DIR))); + + presentation = doLaunchAction(PLAIN_DIR); + assertTrue(presentation.isVisible()); + assertTrue(presentation.isEnabled()); + assertEquals(presentation.getText(), PyBundle.message("python.unmark.as.namespace.package")); + assertFalse(myNspService.isMarked(myFixture.findFileInTempDir(PLAIN_DIR))); + } + + public void testOrdinaryPackage() { + doCopyDirectory(); + + Presentation presentation = doLaunchAction(ORDINARY_PACK_DIR); + assertTrue(presentation.isVisible()); + assertFalse(presentation.isEnabled()); + assertFalse(myNspService.isMarked(myFixture.findFileInTempDir(ORDINARY_PACK_DIR))); + } + + public void testNestedNamespacePackage() { + doCopyDirectory(); + myNspService.toggleMarkingAsNamespacePackage(myFixture.findFileInTempDir(NAMESPACE_PACK_DIR)); + assertTrue(myNspService.isMarked(myFixture.findFileInTempDir(NAMESPACE_PACK_DIR))); + assertTrue(myNspService.isNamespacePackage(myFixture.findFileInTempDir(NAMESPACE_PACK_DIR + "/nestedNamespacePackage"))); + + Presentation presentation = doLaunchAction(NAMESPACE_PACK_DIR + "/nestedNamespacePackage"); + assertTrue(presentation.isVisible()); + assertFalse(presentation.isEnabled()); + } + + public void testPlainDirectoryInOrdinaryPackage() { + doCopyDirectory(); + + Presentation presentation = doLaunchAction(ORDINARY_PACK_DIR + "/nestedPlainDirectory"); + assertTrue(presentation.isVisible()); + assertTrue(presentation.isEnabled()); + assertEquals(presentation.getText(), PyBundle.message("python.namespace.package.folder")); + assertTrue(myNspService.isMarked(myFixture.findFileInTempDir(ORDINARY_PACK_DIR + "/nestedPlainDirectory"))); + + presentation = doLaunchAction(ORDINARY_PACK_DIR + "/nestedPlainDirectory"); + assertTrue(presentation.isVisible()); + assertTrue(presentation.isEnabled()); + assertEquals(presentation.getText(), PyBundle.message("python.unmark.as.namespace.package")); + assertFalse(myNspService.isMarked(myFixture.findFileInTempDir(ORDINARY_PACK_DIR + "/nestedPlainDirectory"))); + } + + private @NotNull Presentation doLaunchAction(@NotNull String directoryPath) { + MapDataContext mapDataContext = new MapDataContext(); + mapDataContext.put(CommonDataKeys.VIRTUAL_FILE_ARRAY, new VirtualFile[] {myFixture.findFileInTempDir(directoryPath)}); + mapDataContext.put(LangDataKeys.MODULE, myFixture.getModule()); + + AnAction action = new PyMarkAsNamespacePackageAction(); + TestActionEvent e = new TestActionEvent(mapDataContext, action); + action.beforeActionPerformedUpdate(e); + if (e.getPresentation().isEnabledAndVisible()) { + action.actionPerformed(e); + } + + return e.getPresentation(); + } + + private void doCopyDirectory() { + myFixture.copyDirectoryToProject(getTestName(false), ""); + } + + @Override + protected String getTestDataPath() { + return PythonTestUtil.getTestDataPath() + "/actions/MarkAsNamespacePackage"; + } +} diff --git a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java index 3d98fa5dc8fc..5b7b61d24d44 100644 --- a/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java +++ b/python/testSrc/com/jetbrains/python/fixtures/PyTestCase.java @@ -48,6 +48,7 @@ import com.jetbrains.python.PythonTestUtil; import com.jetbrains.python.documentation.PyDocumentationSettings; import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.documentation.docstrings.DocStringFormat; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyFileImpl; import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; @@ -265,6 +266,7 @@ public abstract class PyTestCase extends UsefulTestCase { @Override protected void tearDown() throws Exception { try { + PyNamespacePackagesService.getInstance(myFixture.getModule()).resetAllNamespacePackages(); setLanguageLevel(null); myFixture.tearDown(); myFixture = null; diff --git a/python/testSrc/com/jetbrains/python/inspections/PyRelativeImportInspectionTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyRelativeImportInspectionTest.kt new file mode 100644 index 000000000000..84bdfdd90fc7 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyRelativeImportInspectionTest.kt @@ -0,0 +1,149 @@ +// Copyright 2000-2020 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.inspections + +import com.intellij.application.options.RegistryManager +import com.jetbrains.python.PyBundle +import com.jetbrains.python.fixtures.PyInspectionTestCase +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService +import com.jetbrains.python.psi.LanguageLevel +import org.jetbrains.annotations.NonNls + +class PyRelativeImportInspectionTest: PyInspectionTestCase() { + override fun setUp() { + super.setUp() + setLanguageLevel(LanguageLevel.getLatest()) + RegistryManager.getInstance()["python.explicit.namespace.packages"].resetToDefault() + } + + override fun tearDown() { + setLanguageLevel(LanguageLevel.getDefault()) + RegistryManager.getInstance()["python.explicit.namespace.packages"].resetToDefault() + super.tearDown() + } + + override fun getInspectionClass(): Class { + return PyRelativeImportInspection::class.java + } + + fun testPlainDirectoryDottedImportDeleteDot() { + doRelativeImportInspectionTest("$PLAIN_DIR/dottedImport.py", PyBundle.message("QFIX.change.to.same.directory.import")) + } + + fun testPlainDirectoryDottedImportMarkDirectory() { + doRelativeImportInspectionTest("$PLAIN_DIR/dottedImport.py", PyBundle.message("QFIX.mark.as.namespace.package", PLAIN_DIR)) + val service = PyNamespacePackagesService.getInstance(myFixture.module) + val plainDirVirtualFile = myFixture.findFileInTempDir(PLAIN_DIR) + assertTrue(service.isMarked(plainDirVirtualFile)) + } + + fun testPlainDirectoryInsidePackageInsidePlainDirectoryNoInspection() { + doRelativeImportInspectionTest("$PLAIN_DIR/ordinaryPackage/nestedPlainDirectory/dottedImport.py") + } + + fun testNestedPlainDirectoryDottedImportMarkDirectory() { + doRelativeImportInspectionTest("$PLAIN_DIR/nestedPlainDirectory/dottedImport.py", PyBundle.message("QFIX.mark.as.namespace.package", + PLAIN_DIR)) + val service = PyNamespacePackagesService.getInstance(myFixture.module) + val plainDirVirtualFile = myFixture.findFileInTempDir(PLAIN_DIR) + val nestedPlainDirVirtualFile = myFixture.findFileInTempDir("$PLAIN_DIR/nestedPlainDirectory") + assertTrue(service.isMarked(plainDirVirtualFile)) + assertFalse(service.isMarked(nestedPlainDirVirtualFile)) + assertTrue(service.isNamespacePackage(plainDirVirtualFile)) + assertTrue(service.isNamespacePackage(nestedPlainDirVirtualFile)) + } + + fun testPlainDirectoryDottedImportRegistryOffNoInspection() { + RegistryManager.getInstance()["python.explicit.namespace.packages"].setValue(false) + doMultiFileTest("$PLAIN_DIR/dottedImport.py") + } + + fun testSourceRootDottedImportInspectionWithoutQuickFixes() { + myFixture.copyDirectoryToProject(testDirectoryPath, "") + runWithSourceRoots(listOf(myFixture.findFileInTempDir("$PLAIN_DIR/sourceRoot"))) { + val currentFile = myFixture.configureFromTempProjectFile("$PLAIN_DIR/sourceRoot/script.py") + configureInspection() + assertProjectFilesNotParsed(currentFile) + assertSdkRootsNotParsed(currentFile) + assertEmpty(myFixture.filterAvailableIntentions(PyBundle.message("QFIX.mark.as.namespace.package", PLAIN_DIR))) + assertEmpty(myFixture.filterAvailableIntentions(PyBundle.message("QFIX.mark.as.namespace.package", "$PLAIN_DIR/sourceRoot"))) + assertEmpty(myFixture.filterAvailableIntentions(PyBundle.message("QFIX.change.to.same.directory.import"))) + } + } + + fun testNamespacePackageSameDirectoryImportNoInspection() { + doNamespacePackageTest("$NAMESPACE_PACK_DIR/mod.py", NAMESPACE_PACK_DIR) + } + + fun testNamespacePackageDottedImportNoInspection() { + doNamespacePackageTest("$NAMESPACE_PACK_DIR/mod.py", NAMESPACE_PACK_DIR) + } + + fun testNamespacePackageSameDirectoryImportRegistryOffNoInspection() { + RegistryManager.getInstance()["python.explicit.namespace.packages"].setValue(false) + doNamespacePackageTest("$NAMESPACE_PACK_DIR/mod.py", NAMESPACE_PACK_DIR) + } + + fun testNestedNamespacePackageSameDirectoryImportRegistryOffNoInspection() { + RegistryManager.getInstance()["python.explicit.namespace.packages"].setValue(false) + doNamespacePackageTest("$NAMESPACE_PACK_DIR/nestedNamespacePackage/mod.py", NAMESPACE_PACK_DIR) + } + + fun testNotMarkedNamespacePackageInsidePackageSameDirectoryImportNoInspection() { + doNamespacePackageTest("$NAMESPACE_PACK_DIR/$ORDINARY_PACK_DIR/nestedNamespacePackage/mod.py", NAMESPACE_PACK_DIR) + } + + fun testOrdinaryPackageSameDirectoryImportNoInspection() { + doRelativeImportInspectionTest("$ORDINARY_PACK_DIR/script.py") + } + + fun testOrdinaryPackageDottedImportNoInspection() { + doRelativeImportInspectionTest("$ORDINARY_PACK_DIR/script.py") + } + + fun testPython2PlainDirectoryNoInspection() { + runWithLanguageLevel(LanguageLevel.PYTHON27) { + doRelativeImportInspectionTest("$PLAIN_DIR/script.py") + } + } + + private fun doRelativeImportInspectionTest(filename: String, hint: String? = null) { + doMultiFileTest(filename) + if (hint != null) { + val intentionAction = myFixture.findSingleIntention(hint) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkHighlighting(isWarning, isInfo, isWeakWarning) + myFixture.checkResultByFile(filename, getExpectedFilePathAfterFix(filename), true) + } + } + + private fun doNamespacePackageTest(filename: String, directoryToMark: String, hint: String? = null) { + myFixture.copyDirectoryToProject(testDirectoryPath, "") + toggleNamespacePackageDirectory(directoryToMark) + val currentFile = myFixture.configureFromTempProjectFile(filename) + configureInspection() + assertProjectFilesNotParsed(currentFile) + assertSdkRootsNotParsed(currentFile) + if (hint != null) { + val intentionAction = myFixture.findSingleIntention(hint) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction) + myFixture.checkResultByFile(filename, getExpectedFilePathAfterFix(filename), true) + } + } + + private fun toggleNamespacePackageDirectory(directory: String) { + PyNamespacePackagesService + .getInstance(myFixture.module) + .toggleMarkingAsNamespacePackage(myFixture.findFileInTempDir(directory)) + } + + private fun getExpectedFilePathAfterFix(originalFileName: String): @NonNls String = + "$testDirectoryPath/${originalFileName.removeSuffix(".py")}_after.py" + + companion object { + private const val PLAIN_DIR = "plainDirectory" + private const val NAMESPACE_PACK_DIR = "namespacePackage" + private const val ORDINARY_PACK_DIR = "ordinaryPackage" + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java index d25bc7eaf408..ea9f000f4c10 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java @@ -18,6 +18,7 @@ import com.jetbrains.python.PythonTestUtil; import com.jetbrains.python.codeInsight.PyCodeInsightSettings; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.formatter.PyCodeStyleSettings; +import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.stubs.PyClassNameIndex; import com.jetbrains.python.psi.stubs.PyFunctionNameIndex; @@ -28,6 +29,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -203,7 +205,29 @@ public class PyMoveTest extends PyTestCase { // PY-14384 public void testRelativeImportInsideNamespacePackage() { - runWithLanguageLevel(LanguageLevel.PYTHON34, () -> doMoveFileTest("nspkg/nssubpkg", "")); + runWithLanguageLevel(LanguageLevel.PYTHON34, () -> { + String fileName = "nspkg/nssubpkg"; + String toDirName = ""; + doComparingDirectories(testDir -> { + PyNamespacePackagesService.getInstance(myFixture.getModule()).toggleMarkingAsNamespacePackage(testDir.findFileByRelativePath("nspkg")); + + final Project project = myFixture.getProject(); + final PsiManager manager = PsiManager.getInstance(project); + final VirtualFile virtualFile = testDir.findFileByRelativePath(fileName); + assertNotNull(virtualFile); + PsiElement file = manager.findFile(virtualFile); + if (file == null) { + file = manager.findDirectory(virtualFile); + } + assertNotNull(file); + final VirtualFile toVirtualDir = testDir.findFileByRelativePath(toDirName); + assertNotNull(toVirtualDir); + final PsiDirectory toDir = manager.findDirectory(toVirtualDir); + new MoveFilesOrDirectoriesProcessor(project, new PsiElement[]{file}, toDir, false, false, null, null).run(); + + PyNamespacePackagesService.getInstance(myFixture.getModule()).setNamespacePackageFolders(new ArrayList<>()); + }); + }); } // PY-14384