diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml
index 08ad91ac7121..e8f72f7bc332 100644
--- a/python/src/META-INF/python-plugin-common.xml
+++ b/python/src/META-INF/python-plugin-common.xml
@@ -334,6 +334,7 @@
+
diff --git a/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java b/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java
index 7f630a3ef93e..683642f623a8 100644
--- a/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java
+++ b/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java
@@ -194,7 +194,12 @@ public class PyClassRefactoringUtil {
if (target == null) return;
if (PyBuiltinCache.getInstance(target).hasInBuiltins(target)) return;
if (PsiTreeUtil.isAncestor(node.getContainingFile(), target, false)) return;
- insertImport(node, target, asName);
+ if (target instanceof PyFile) {
+ insertImport(node, target, asName, false);
+ }
+ else {
+ insertImport(node, target, asName);
+ }
node.putCopyableUserData(ENCODED_IMPORT, null);
node.putCopyableUserData(ENCODED_IMPORT_AS, null);
}
@@ -232,23 +237,28 @@ public class PyClassRefactoringUtil {
public static void insertImport(PsiElement anchor, PsiNamedElement element, @Nullable String asName, boolean preferFromImport) {
if (PyBuiltinCache.getInstance(element).hasInBuiltins(element)) return;
final PsiFile newFile = element.getContainingFile();
- final VirtualFile vFile = newFile.getVirtualFile();
- assert vFile != null;
final PsiFile file = anchor.getContainingFile();
if (newFile == file) return;
- final PyQualifiedName qName = ResolveImportUtil.findCanonicalImportPath(element, anchor);
- assert isValidQualifiedName(qName);
- final String importableName = (qName != null) ? qName.toString() : null;
+ final PyQualifiedName qname = ResolveImportUtil.findCanonicalImportPath(element, anchor);
+ if (qname == null || !isValidQualifiedName(qname)) {
+ return;
+ }
+ final PyQualifiedName containingQName;
+ final String importedName;
+ if (element instanceof PyFile) {
+ containingQName = qname.removeLastComponent();
+ importedName = qname.getLastComponent();
+ }
+ else {
+ containingQName = qname;
+ importedName = getOriginalName(element);
+ }
final AddImportHelper.ImportPriority priority = AddImportHelper.getImportPriority(anchor, newFile);
- if (!preferFromImport || element instanceof PyFile) {
- if (element instanceof PyFile) {
- AddImportHelper.addImportStatement(file, importableName, asName, priority);
- } else {
- final String name = element.getName();
- AddImportHelper.addImportStatement(file, importableName + "." + name, asName, priority);
- }
- } else {
- AddImportHelper.addImportFrom(file, importableName, element.getName(), asName, priority);
+ if (preferFromImport && !containingQName.getComponents().isEmpty()) {
+ AddImportHelper.addImportFrom(file, containingQName.toString(), importedName, asName, priority);
+ }
+ else {
+ AddImportHelper.addImportStatement(file, containingQName.append(importedName).toString(), asName, priority);
}
}
@@ -306,7 +316,7 @@ public class PyClassRefactoringUtil {
}
public static void updateImportOfElement(PyImportStatementBase importStatement, PsiNamedElement element) {
- final String name = element.getName();
+ final String name = getOriginalName(element);
if (name != null) {
PyImportElement importElement = null;
for (PyImportElement e: importStatement.getImportElements()) {
@@ -327,8 +337,23 @@ public class PyClassRefactoringUtil {
}
@Nullable
- private static String getOriginalName(PyImportElement e) {
- final PyQualifiedName qname = e.getImportedQName();
+ public static String getOriginalName(PsiNamedElement element) {
+ if (element instanceof PyFile) {
+ final PsiElement e = PyUtil.turnInitIntoDir(element);
+ if (e instanceof PsiFileSystemItem) {
+ final VirtualFile virtualFile = ((PsiFileSystemItem)e).getVirtualFile();
+ if (virtualFile != null) {
+ return virtualFile.getNameWithoutExtension();
+ }
+ }
+ return null;
+ }
+ return element.getName();
+ }
+
+ @Nullable
+ private static String getOriginalName(PyImportElement element) {
+ final PyQualifiedName qname = element.getImportedQName();
if (qname != null && qname.getComponentCount() > 0) {
return qname.getComponents().get(0);
}
diff --git a/python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java b/python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java
new file mode 100644
index 000000000000..3bce02aef7a3
--- /dev/null
+++ b/python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java
@@ -0,0 +1,104 @@
+package com.jetbrains.python.refactoring.move;
+
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.*;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFileHandler;
+import com.intellij.usageView.UsageInfo;
+import com.intellij.util.IncorrectOperationException;
+import com.jetbrains.python.codeInsight.imports.PyImportOptimizer;
+import com.jetbrains.python.psi.*;
+import com.jetbrains.python.psi.impl.PyQualifiedName;
+import com.jetbrains.python.psi.resolve.ResolveImportUtil;
+import com.jetbrains.python.refactoring.PyRefactoringUtil;
+import com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author vlan
+ */
+public class PyMoveFileHandler extends MoveFileHandler {
+ private static final Key REFERENCED_ELEMENT = Key.create("PY_REFERENCED_ELEMENT");
+
+ @Override
+ public boolean canProcessElement(PsiFile element) {
+ return element instanceof PyFile;
+ }
+
+ @Override
+ public void prepareMovedFile(PsiFile file, PsiDirectory moveDestination, Map oldToNewMap) {
+ // TODO: Update relative imports
+ }
+
+ @Override
+ public List findUsages(PsiFile file, PsiDirectory newParent, boolean searchInComments, boolean searchInNonJavaFiles) {
+ if (file != null) {
+ final List usages = PyRefactoringUtil.findUsages(file);
+ for (UsageInfo usage : usages) {
+ final PsiElement element = usage.getElement();
+ if (element != null) {
+ element.putCopyableUserData(REFERENCED_ELEMENT, file);
+ }
+ }
+ return usages;
+ }
+ return null;
+ }
+
+ @Override
+ public void retargetUsages(List usages, Map oldToNewMap) {
+ final Set updatedFiles = new HashSet();
+ for (UsageInfo usage : usages) {
+ final PsiElement element = usage.getElement();
+ if (element != null) {
+ // TODO: Check if the element still exists/valid
+ final PsiNamedElement newElement = element.getCopyableUserData(REFERENCED_ELEMENT);
+ element.putCopyableUserData(REFERENCED_ELEMENT, null);
+ if (newElement != null) {
+ final PsiFile file = element.getContainingFile();
+ final PyImportStatementBase importStmt = PsiTreeUtil.getParentOfType(usage.getElement(), PyImportStatementBase.class);
+ // TODO: Retarget qualified expressions in docstrings
+ if (importStmt != null) {
+ PyClassRefactoringUtil.updateImportOfElement(importStmt, newElement);
+ final PyQualifiedName newElementName = ResolveImportUtil.findCanonicalImportPath(newElement, element);
+ replaceWithQualifiedExpression(element, newElementName);
+ updatedFiles.add(file);
+ }
+ else if (element instanceof PyReferenceExpression) {
+ final PyQualifiedName newElementName = PyQualifiedName.fromComponents(PyClassRefactoringUtil.getOriginalName(newElement));
+ replaceWithQualifiedExpression(element, newElementName);
+ PyClassRefactoringUtil.insertImport(element, newElement, null);
+ updatedFiles.add(file);
+ }
+ }
+ }
+ }
+ if (!updatedFiles.isEmpty()) {
+ final PyImportOptimizer optimizer = new PyImportOptimizer();
+ for (PsiFile file : updatedFiles) {
+ optimizer.processFile(file).run();
+ }
+ }
+ }
+
+ private static void replaceWithQualifiedExpression(@NotNull PsiElement oldElement,
+ @Nullable PyQualifiedName newElementName) {
+ if (newElementName != null && PyClassRefactoringUtil.isValidQualifiedName(newElementName)) {
+ final PyElementGenerator generator = PyElementGenerator.getInstance(oldElement.getProject());
+ final PsiElement newElement = generator.createExpressionFromText(LanguageLevel.forElement(oldElement), newElementName.toString());
+ if (newElement != null) {
+ oldElement.replace(newElement);
+ }
+ }
+ }
+
+ @Override
+ public void updateMovedFile(PsiFile file) throws IncorrectOperationException {
+ }
+}
diff --git a/python/testData/refactoring/move/module/after/src/a.py b/python/testData/refactoring/move/module/after/src/a.py
new file mode 100644
index 000000000000..821e48bdf42b
--- /dev/null
+++ b/python/testData/refactoring/move/module/after/src/a.py
@@ -0,0 +1,7 @@
+from p1 import m1
+from p1.m1 import foo
+
+m1.foo
+m1.bar
+m1
+foo
\ No newline at end of file
diff --git a/python/testData/refactoring/move/module/after/src/p1/__init__.py b/python/testData/refactoring/move/module/after/src/p1/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/refactoring/move/module/after/src/p1/m1.py b/python/testData/refactoring/move/module/after/src/p1/m1.py
new file mode 100644
index 000000000000..cb19528f954f
--- /dev/null
+++ b/python/testData/refactoring/move/module/after/src/p1/m1.py
@@ -0,0 +1,2 @@
+foo = 42
+bar = -1
\ No newline at end of file
diff --git a/python/testData/refactoring/move/module/after/src/p1/p2/__init__.py b/python/testData/refactoring/move/module/after/src/p1/p2/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/refactoring/move/module/before/src/a.py b/python/testData/refactoring/move/module/before/src/a.py
new file mode 100644
index 000000000000..89193abbbec7
--- /dev/null
+++ b/python/testData/refactoring/move/module/before/src/a.py
@@ -0,0 +1,9 @@
+from p1.p2 import m1
+from p1.p2.m1 import foo
+import p1.p2.m1
+import p1
+
+p1.p2.m1.foo
+m1.bar
+m1
+foo
\ No newline at end of file
diff --git a/python/testData/refactoring/move/module/before/src/p1/__init__.py b/python/testData/refactoring/move/module/before/src/p1/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/refactoring/move/module/before/src/p1/p2/__init__.py b/python/testData/refactoring/move/module/before/src/p1/p2/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/testData/refactoring/move/module/before/src/p1/p2/m1.py b/python/testData/refactoring/move/module/before/src/p1/p2/m1.py
new file mode 100644
index 000000000000..cb19528f954f
--- /dev/null
+++ b/python/testData/refactoring/move/module/before/src/p1/p2/m1.py
@@ -0,0 +1,2 @@
+foo = 42
+bar = -1
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java
index 6f2cad79bdce..8451e7f720a3 100644
--- a/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java
+++ b/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java
@@ -1,9 +1,10 @@
package com.jetbrains.python.refactoring;
+import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.PsiDocumentManager;
-import com.intellij.psi.PsiNamedElement;
+import com.intellij.psi.*;
+import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesProcessor;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.util.SystemProperties;
import com.jetbrains.python.PythonTestUtil;
@@ -69,7 +70,44 @@ public class PyMoveTest extends PyTestCase {
doMoveSymbolTest("B", "b.py");
}
- private void doMoveSymbolTest(final String symbolName, final String toFileName) {
+ // PY-4379
+ public void testModule() {
+ doMoveFileTest("p1/p2/m1.py", "p1");
+ }
+
+ private void doMoveFileTest(String fileName, String toDirName) {
+ Project project = myFixture.getProject();
+ PsiManager manager = PsiManager.getInstance(project);
+
+ String root = "/refactoring/move/" + getTestName(true);
+ String rootBefore = root + "/before/src";
+ String rootAfter = root + "/after/src";
+
+ VirtualFile dir1 = myFixture.copyDirectoryToProject(rootBefore, "");
+ PsiDocumentManager.getInstance(project).commitAllDocuments();
+
+ VirtualFile virtualFile = dir1.findFileByRelativePath(fileName);
+ assertNotNull(virtualFile);
+ PsiElement file = manager.findFile(virtualFile);
+ if (file == null) {
+ file = manager.findDirectory(virtualFile);
+ }
+ assertNotNull(file);
+ VirtualFile toVirtualDir = dir1.findFileByRelativePath(toDirName);
+ assertNotNull(toVirtualDir);
+ PsiDirectory toDir = manager.findDirectory(toVirtualDir);
+ new MoveFilesOrDirectoriesProcessor(project, new PsiElement[] {file}, toDir, false, false, null, null).run();
+
+ VirtualFile dir2 = getVirtualFileByName(PythonTestUtil.getTestDataPath() + rootAfter);
+ try {
+ PlatformTestUtil.assertDirectoriesEqual(dir2, dir1, null);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void doMoveSymbolTest(String symbolName, String toFileName) {
String root = "/refactoring/move/" + getTestName(true);
String rootBefore = root + "/before/src";
String rootAfter = root + "/after/src";