relative imports should be inserted instead of absolute ones when possible (PY-6054)

If the file already has relative imports, next one inserted via completion/auto-import has a chance to be relative. For that new import has to have the number of dots <= than the max of the file's relative import and a number provided by a registry value in 'python.relative.import.depth', that is 3 by default. It also has to be located in the same root package, since python doesn't allow relative imports to step outside of it. The file must not have "if __name__ == '__main__'" in it, since it suggests that it is intended to be runnable and relative imports will not work.
Relative import statements can also be updated with new import elements, if the user's codestyle allows it.

GitOrigin-RevId: 766a03e2252bb1f8193a2156831cc8fdf0a7aced
This commit is contained in:
Aleksei Kniazev
2019-10-02 10:07:03 +00:00
committed by intellij-monorepo-bot
parent 1d13116e16
commit 2d72c69170
46 changed files with 184 additions and 2 deletions
@@ -9,6 +9,7 @@ import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
@@ -29,6 +30,7 @@ import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
import com.jetbrains.python.pyi.PyiFile;
import com.jetbrains.python.pyi.PyiUtil;
import com.jetbrains.python.sdk.PythonSdkUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -514,14 +516,35 @@ public class AddImportHelper {
@Nullable String asName,
@Nullable ImportPriority priority,
@Nullable PsiElement anchor) {
final List<PyFromImportStatement> existingImports = ((PyFile)file).getFromImports();
final PyFile pyFile = (PyFile)file;
final List<PyFromImportStatement> existingImports = pyFile.getFromImports();
int relativeLevel = 0;
PyRelativeImportData importData = null;
if (priority == ImportPriority.PROJECT) {
importData = PyRelativeImportData.fromString(from, pyFile);
if (importData != null) {
relativeLevel = importData.getRelativeLevel();
}
}
if (!PythonCodeStyleService.getInstance().isOptimizeImportsAlwaysSplitFromImports(file)) {
for (PyFromImportStatement existingImport : existingImports) {
if (existingImport.isStarImport()) {
continue;
}
final String existingSource = Objects.toString(existingImport.getImportSourceQName(), "");
if (from.equals(existingSource) && existingImport.getRelativeLevel() == 0) {
boolean updateExisting = false;
final int currentRelativeLevel = existingImport.getRelativeLevel();
if (currentRelativeLevel == 0) {
updateExisting = from.equals(existingSource);
}
else if (relativeLevel != 0) {
updateExisting = currentRelativeLevel == relativeLevel && existingSource.equals(importData.getRelativeLocation());
}
if (updateExisting) {
for (PyImportElement el : existingImport.getImportElements()) {
final String existingName = Objects.toString(el.getImportedQName(), "");
if (name.equals(existingName) && Comparing.equal(asName, el.getAsName())) {
@@ -537,6 +560,21 @@ public class AddImportHelper {
}
}
}
if (!PyUtil.hasIfNameEqualsMain(pyFile)) {
final int maxRelativeLevelInFile = StreamEx.of(pyFile.getFromImports())
.mapToInt(PyFromImportStatement::getRelativeLevel)
.max()
.orElse(0);
if (maxRelativeLevelInFile > 0 && relativeLevel > 0) {
int maxAllowedDepth = Math.max(maxRelativeLevelInFile, Registry.intValue("python.relative.import.depth"));
if (maxAllowedDepth >= relativeLevel) {
from = importData.getLocationWithDots();
}
}
}
addFromImportStatement(file, from, name, asName, priority, anchor);
return true;
}
@@ -0,0 +1,36 @@
package com.jetbrains.python.codeInsight.imports
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.PyNames
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.resolve.QualifiedNameFinder
/**
* @author Aleksei Kniazev
*/
class PyRelativeImportData private constructor(val relativeLocation: String, val relativeLevel: Int) {
val locationWithDots: String
get() = ".".repeat(relativeLevel) + relativeLocation
companion object {
@JvmStatic
fun fromString(location: String, file: PyFile): PyRelativeImportData? {
val qName = QualifiedName.fromDottedString(location)
val fileQName = QualifiedNameFinder.findCanonicalImportPath(file, null) ?: return null
if (qName.firstComponent != fileQName.firstComponent) return null
val common = fileQName.components.asSequence()
.zip(qName.components.asSequence()) { s1, s2 -> s1 == s2 }
.takeWhile { it }
.count()
val remainingFileQname = fileQName.removeHead(common)
val remainingQname = qName.removeHead(common)
val implicitLevel = if (file.name == PyNames.INIT_DOT_PY) 1 else 0
return PyRelativeImportData(remainingQname.toString(), remainingFileQname.componentCount + implicitLevel)
}
}
}
@@ -290,6 +290,14 @@ public class PyUtil {
return element instanceof PyTargetExpression && ScopeUtil.getScopeOwner(element) instanceof PyClass;
}
public static boolean hasIfNameEqualsMain(@NotNull PyFile file) {
final PyIfStatement dunderMain = SyntaxTraverser.psiApi()
.children(file)
.filterMap(psi -> psi instanceof PyIfStatement ? ((PyIfStatement)psi) : null)
.find(ifStatement -> isIfNameEqualsMain(ifStatement));
return dunderMain != null;
}
public static boolean isIfNameEqualsMain(PyIfStatement ifStatement) {
final PyExpression condition = ifStatement.getIfPart().getCondition();
return isNameEqualsMain(condition);
@@ -619,6 +619,7 @@
<applicationService serviceInterface="com.jetbrains.python.PythonRuntimeService" serviceImplementation="com.jetbrains.python.PythonRuntimeServiceImpl" overrides="true"/>
<applicationService serviceInterface="com.jetbrains.python.PythonCodeStyleService" serviceImplementation="com.jetbrains.python.PythonCodeStyleServiceImpl" overrides="true"/>
<registryKey key="python.relative.import.depth" defaultValue="3" description="Specifies default acceptable number of dots in a relative import statement"/>
</extensions>
<extensionPoints>
@@ -0,0 +1,6 @@
def oh():
pass
def no():
pass
@@ -0,0 +1 @@
from .bar import oh, no
@@ -0,0 +1 @@
from .bar import oh
@@ -0,0 +1,2 @@
def bar_func():
pass
@@ -0,0 +1,2 @@
def baz_func():
pass
@@ -0,0 +1,2 @@
from .baz import baz_func
from .bar import bar_func
@@ -0,0 +1 @@
from .bar import bar_func
@@ -0,0 +1,2 @@
def bar_func():
pass
@@ -0,0 +1,2 @@
def baz_func():
pass
@@ -0,0 +1,5 @@
from foo.baz import baz_func
from .bar import bar_func
if __name__ == '__main__':
pass
@@ -0,0 +1,4 @@
from .bar import bar_func
if __name__ == '__main__':
pass
@@ -0,0 +1,2 @@
def bar_func():
pass
@@ -0,0 +1,2 @@
def func():
pass
@@ -0,0 +1,2 @@
from ..src.baz import func
from ..bar import bar_func
@@ -0,0 +1 @@
from ..bar import bar_func
@@ -0,0 +1,2 @@
def foo_func():
pass
@@ -0,0 +1,2 @@
def bar_func():
pass
@@ -0,0 +1,2 @@
from pkg1.foo import foo_func
from .. import bar_func
@@ -0,0 +1 @@
from .. import bar_func
@@ -0,0 +1,2 @@
def bar_func():
pass
@@ -0,0 +1,2 @@
def foo_func():
pass
@@ -0,0 +1,2 @@
from ....foo import foo_func
from ....bar import bar_func
@@ -0,0 +1 @@
from ....bar import bar_func
@@ -0,0 +1,2 @@
def baz_func():
pass
@@ -0,0 +1,2 @@
from .. import lib
from .baz import baz_func
@@ -0,0 +1 @@
from .baz import baz_func
@@ -132,6 +132,41 @@ public class PyAddImportTest extends PyTestCase {
doAddFromImport("collections", "OrderedDict", BUILTIN);
}
// PY-6054
public void testRelativeImportFromSamePackage() {
doTestRelativeImport("foo.baz", "baz_func", "foo/test");
}
// PY-6054
public void testRelativeImportFromAlreadyImportedModule() {
doTestRelativeImport("foo.bar", "no", "foo/test");
}
// PY-6054
public void testRelativeImportInInitFile() {
doTestRelativeImport("foo.src.baz", "func", "foo/test/__init__");
}
// PY-6054
public void testRelativeImportInFileWithMain() {
doTestRelativeImport("foo.baz", "baz_func", "foo/test");
}
// PY-6054
public void testRelativeImportTooDeep() {
doTestRelativeImport("pkg1.foo", "foo_func", "pkg1/pkg2/pkg3/pkg4/test");
}
// PY-6054
public void testRelativeImportTooDeepWithSameLevelUsed() {
doTestRelativeImport("pkg1.foo", "foo_func", "pkg1/pkg2/pkg3/pkg4/test");
}
// PY-6054
public void testRelativeImportWithDotsOnly() {
doTestRelativeImport("foo", "lib", "foo/bar/test");
}
private void doAddOrUpdateFromImport(final String path, final String name, final ImportPriority priority) {
myFixture.configureByFile(getTestName(true) + ".py");
WriteCommandAction.runWriteCommandAction(myFixture.getProject(), () -> {
@@ -184,6 +219,16 @@ public class PyAddImportTest extends PyTestCase {
myFixture.checkResultByFile(getTestName(true) + ".after.py");
}
private void doTestRelativeImport(final @NotNull String from, final @NotNull String name, final @NotNull String file) {
final String testName = getTestName(true);
myFixture.copyDirectoryToProject(testName, "");
myFixture.configureByFile(file + ".py");
WriteCommandAction.runWriteCommandAction(myFixture.getProject(), () -> {
AddImportHelper.addOrUpdateFromImportStatement(myFixture.getFile(), from, name, null, PROJECT, null);
});
myFixture.checkResultByFile(testName + "/" + file + ".after.py");
}
@Override
protected String getTestDataPath() {
return super.getTestDataPath() + "/addImport";