mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
584d134693
commit
c5ae9550ce
@@ -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) }
|
||||
}
|
||||
}
|
||||
+26
@@ -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();
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print("ok")
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from .util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print("ok")
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print("ok")
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print("ok")
|
||||
+1
@@ -0,0 +1 @@
|
||||
<weak_warning descr="Relative import outside of a package">from .util import foo</weak_warning>
|
||||
+1
@@ -0,0 +1 @@
|
||||
from .util import foo
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+1
@@ -0,0 +1 @@
|
||||
from .util import foo
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+1
@@ -0,0 +1 @@
|
||||
from util import foo
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+1
@@ -0,0 +1 @@
|
||||
<weak_warning descr="Relative import outside of a package">from .util import foo</weak_warning>
|
||||
+1
@@ -0,0 +1 @@
|
||||
from util import foo
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+1
@@ -0,0 +1 @@
|
||||
<weak_warning descr="Relative import outside of a package">from .util import foo</weak_warning>
|
||||
+1
@@ -0,0 +1 @@
|
||||
from .util import foo
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+1
@@ -0,0 +1 @@
|
||||
from .util import foo
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from .util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from .util import foo
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<weak_warning descr="Relative import outside of a package">from ..util import foo</weak_warning>
|
||||
foo()
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
@@ -1,4 +1,4 @@
|
||||
from .test import Test1, Base
|
||||
from test import Test1, Base
|
||||
|
||||
b = Base(a=2)
|
||||
t = Test1(a=2)
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from . import util
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import util
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from . import util
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import util
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from .util import foo
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from .util import foo
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from .util import foo
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
from util import foo
|
||||
# <ref>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
print('ok')
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user