PY-42750 Allow same directory imports in Python 3 and introduce explicit namespace packages

There are 3 types of directories: plain directories, ordinary packages (with
__init__.py) and PEP 420 namespace packages. There are 3 types of imports:
absolute (from a root), same directory (absolute import from the current
directory when it's not explicitly marked as a root) and relative imports
(imports that start with dot).

Absolute imports are correct in all kinds of directories.
Same directory imports are correct in Python 2 in all cases and in Python 3 if
we have the directory containing the script with this import in Python path at
runtime. Users of Python 3 often face the problem when they can run the script
from the console because the directory containing this script got into Python
path but still have red underline and an unresolved reference error in the same
directory import because PyCharm didn't know that this file will be used as a
program's entry point. Previously, the way to fix such a problem was marking it
as a source root. But this action was not so obvious, especially for newcomers.
With this feature, such imports resolve successfully and now it is not necessary
to mark directories as source roots.

Relative imports are correct only in Python 3 namespace or ordinary packages and
should not be used in plain directories. If we have a relative import in plain
directory we highlight it with a weak warning and suggest 2 ways of fixing that:
marking directory as a namespace package explicitly (with quick fix or with Mark
As | Namespace Package) or changing this import to the same directory import
with a quickfix or manually.

Explicitly marking namespace packages can later be used for automatically
running files from them and ordinary packages with "-m".
The new resolve policy and explicit namespace packages can be disabled with the
Registry flag "python.explicit.namespace.packages".

These changes also address PY-40396. Namely, now any directory with __init__.py
inside or explicitly marked as a namespace package has a package icon,
regardless of its name or parents.

GitOrigin-RevId: 310fa562eb60121243cb6d68386ffc3e45c73245
This commit is contained in:
andrey.matveev
2020-10-21 16:04:26 +00:00
committed by intellij-monorepo-bot
parent 584d134693
commit c5ae9550ce
97 changed files with 1302 additions and 86 deletions
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports usages of relative imports inside plain directories, i.e. directories neither containing __init__.py nor explicitly marked as namespace packages.
</body>
</html>
@@ -23,6 +23,10 @@ QFIX.generate.binary.stubs=Generate binary stubs
QFIX.reformat.file=Reformat file
QFIX.bold.html.text=<b>{0}</b>
# 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
@@ -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
@@ -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}.
*/
@@ -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);
@@ -161,6 +161,7 @@
<localInspection language="Python" shortName="PyInterpreterInspection" suppressId="PyInterpreter" bundle="messages.PyPsiBundle" key="INSP.NAME.invalid.interpreter" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyInterpreterInspection"/>
<localInspection language="Python" shortName="PyStubPackagesCompatibilityInspection" suppressId="PyStubPackagesCompatibility" bundle="messages.PyPsiBundle" key="INSP.stub.packages.compatibility" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.codeInsight.typing.PyStubPackagesCompatibilityInspection"/>
<localInspection language="Python" shortName="PyStubPackagesAdvertiser" suppressId="PyStubPackagesAdvertiser" bundle="messages.PyPsiBundle" key="INSP.stub.packages" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.codeInsight.typing.PyStubPackagesAdvertiser"/>
<localInspection language="Python" shortName="PyRelativeImportInspection" suppressId="PyPackages" bundle="messages.PyPsiBundle" key="INSP.NAME.relative.import" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyRelativeImportInspection"/>
<defaultLiveTemplates file="liveTemplates/Python.xml"/>
<liveTemplateContext implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$General"/>
@@ -324,6 +325,11 @@
<moduleService serviceImplementation="com.jetbrains.python.packaging.PyPackageRequirementsSettings$ModuleService"/>
<applicationService serviceImplementation="com.jetbrains.python.packaging.PyPackageRequirementsSettings$AppService"/>
<!-- Namespace packages -->
<moduleService serviceImplementation="com.jetbrains.python.namespacePackages.PyNamespacePackagesService"/>
<registryKey key="python.explicit.namespace.packages" defaultValue="true" restartRequired="true" description="Require marking namespace packages explicitly, treat regular directories as implicit source roots"/>
<registryKey key="python.create.namespace.package.action" defaultValue="false" description="Possibility to create namespace packages from New | Python Package"/>
<!-- typing -->
<multiHostInjector implementation="com.jetbrains.python.codeInsight.typing.PyTypingAnnotationInjector"/>
@@ -361,6 +367,7 @@
<statistics.projectUsagesCollector implementation="com.jetbrains.python.statistics.PyPackageVersionUsagesCollector"/>
<statistics.validation.customValidationRule implementation="com.jetbrains.python.statistics.PyPackageUsagesValidationRule"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.sdk.PySdkToInstallCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.namespacePackages.PyNamespacePackagesStatisticsCollector"/>
<!-- Code-insight IDE bridge -->
<applicationService serviceInterface="com.jetbrains.python.PythonRuntimeService" serviceImplementation="com.jetbrains.python.PythonRuntimeServiceImpl"/>
@@ -537,6 +544,9 @@
<condaSdkCustomizer implementation="com.jetbrains.python.sdk.conda.PyCondaSdkDefaultCustomizer"/>
<customPackageIdentifier implementation="com.jetbrains.python.namespacePackages.PyNamespacePackageCustomIdentifier"/>
<pyRootTypeProvider implementation="com.jetbrains.python.namespacePackages.PyNamespacePackageRootProvider"/>
<!-- Experimental packaging extensions-->
<pySdkProvider implementation="com.jetbrains.python.sdk.pipenv.PyPipEnvSdkProvider"/>
<packageManagerProvider implementation="com.jetbrains.python.sdk.pipenv.PyPipenvPackageManagerProvider"/>
@@ -632,6 +642,10 @@
<add-to-group group-id="EditorPopupMenuDebug" relative-to-action="ForceRunToCursor" anchor="after"/>
</action>
<action id="MarkNamespacePackageDirectory" class="com.jetbrains.python.namespacePackages.PyMarkAsNamespacePackageAction">
<add-to-group group-id="MarkRootGroup" anchor="after" relative-to-action="MarkSourceRoot"/>
</action>
<!--suppress PluginXmlI18n -->
<group id="Internal.Python" internal="true" popup="true" text="Python">
<!--suppress PluginXmlI18n -->
@@ -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<VirtualFile> 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;
}
}
@@ -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<PsiDirectory> 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<PsiDirectory>() {
@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<PsiDirectory> 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<PsiFileSystemItem> 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<PsiDirectory> 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<PsiFileSystemItem> 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<PsiFileSystemItem> 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) {
@@ -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<LocalQuickFix>()
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()
}
}
}
@@ -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<PyRootTypeProvider> 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;
}
};
}
@@ -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) }
}
}
@@ -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
}
@@ -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<ContentEntry, VirtualFilePointer>()
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<ContentEntry, VirtualFilePointer> = 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<VirtualFile> = 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)
}
}
@@ -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<PyNamespacePackagesService> {
private final List<VirtualFile> 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<String> getNamespacePackageFolders() {
removeInvalidNamespacePackageFolders();
return Collections.unmodifiableList(ContainerUtil.map(myNamespacePackageFolders, it -> it.getPath()));
}
@Transient
public @NotNull List<VirtualFile> getNamespacePackageFoldersVirtualFiles() {
removeInvalidNamespacePackageFolders();
return Collections.unmodifiableList(myNamespacePackageFolders);
}
public void setNamespacePackageFolders(@NotNull List<String> 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<VirtualFile> 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();
}
}
@@ -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")
}
}
@@ -1,4 +1,4 @@
from .test import Test1, Base
from test import Test1, Base
b = Base(a=2)
t = Test1(a=2)
@@ -0,0 +1 @@
<weak_warning descr="Relative import outside of a package">from .util import foo</weak_warning>
@@ -0,0 +1 @@
<weak_warning descr="Relative import outside of a package">from .util import foo</weak_warning>
@@ -0,0 +1 @@
<weak_warning descr="Relative import outside of a package">from .util import foo</weak_warning>
@@ -0,0 +1,2 @@
<weak_warning descr="Relative import outside of a package">from ..util import foo</weak_warning>
foo()
@@ -1,4 +1,4 @@
from .test import Test1, Base
from test import Test1, Base
b = Base(a=2)
t = Test1(a=2)
@@ -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";
@@ -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));
}
}
@@ -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";
}
}
@@ -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;
@@ -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<out PyInspection> {
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"
}
}
@@ -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