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
]