intention actions to switch between absolute and relative imports (PY-37858)

GitOrigin-RevId: 8c3c1caeba64ff678c8f1dafdd0fc9a44af3873e
This commit is contained in:
Aleksei Kniazev
2019-10-02 12:03:38 +00:00
committed by intellij-monorepo-bot
parent fccb01ec87
commit adbbcef28a
41 changed files with 251 additions and 0 deletions
@@ -0,0 +1 @@
from <spot>.three</spot> import foo
@@ -0,0 +1 @@
from <spot>one.two.three</spot> import foo
@@ -0,0 +1,7 @@
<html>
<body>
<span>
This intention replaces absolute <code>from</code> import with a relative one.
</span>
</body>
</html>
@@ -0,0 +1 @@
from <spot>one.two.three</spot> import foo
@@ -0,0 +1 @@
from <spot>.three</spot> import foo
@@ -0,0 +1,7 @@
<html>
<body>
<span>
This intention replaces relative <code>from</code> import with an absolute one.
</span>
</body>
</html>
@@ -217,6 +217,16 @@
<category>Python</category>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.PyAbsoluteToRelativeImportIntention</className>
<category>Python</category>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.PyRelativeToAbsoluteImportIntention</className>
<category>Python</category>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.ImportToggleAliasIntention</className>
<category>Python</category>
@@ -298,6 +298,10 @@ INTN.convert.static.method.to.function=Convert static method to function
#PyConvertMethodToPropertyIntention
INTN.convert.method.to.property=Convert method to property
#PyConvertImportIntentionAction
INTN.convert.relative.to.absolute=Convert relative import to absolute
INTN.convert.absolute.to.relative=Convert absolute import to relative
# Conflict checker
CONFLICT.name.$0.obscured=Name ''{0}'' obscured by local definitions
CONFLICT.name.$0.obscured.cannot.convert=Name ''{0}'' obscured. Cannot convert.
@@ -0,0 +1,36 @@
// 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.codeInsight.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.jetbrains.python.codeInsight.imports.PyRelativeImportData
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.resolve.QualifiedNameFinder
/**
* Converts location of 'from one.two.three import foo' to the one relative to the current file, e.g. 'from .three import foo'.
*
* @see PyRelativeToAbsoluteImportIntention
* @author Aleksei.Kniazev
*/
class PyAbsoluteToRelativeImportIntention : PyConvertImportIntentionAction("INTN.convert.absolute.to.relative") {
override fun doInvoke(project: Project, editor: Editor, file: PsiFile) {
val statement = findStatement(file, editor) ?: return
val targetPath = statement.importSourceQName ?: return
val importData = PyRelativeImportData.fromString(targetPath.toString(), file as PyFile) ?: return
replaceImportStatement(statement, file, importData.locationWithDots)
}
override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean {
if (file !is PyFile) return false
val statement = findStatement(file, editor) ?: return false
if (statement.relativeLevel != 0) return false
val targetPath = statement.importSourceQName ?: return false
val filePath = QualifiedNameFinder.findCanonicalImportPath(file, null) ?: return false
return targetPath.firstComponent == filePath.firstComponent
}
}
@@ -0,0 +1,38 @@
// 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.codeInsight.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.psi.PyElementGenerator
import com.jetbrains.python.psi.PyFromImportStatement
import com.jetbrains.python.psi.PyStatement
import org.jetbrains.annotations.PropertyKey
abstract class PyConvertImportIntentionAction(@PropertyKey(resourceBundle = "com.jetbrains.python.PyBundle") intentionText: String) : PyBaseIntentionAction() {
init {
text = PyBundle.message(intentionText)
}
override fun getFamilyName(): String = text
fun replaceImportStatement(statement: PyFromImportStatement, file: PsiFile, path: String) {
val imported = statement.importElements.joinToString(", ") { it.text }
val generator = PyElementGenerator.getInstance(file.project)
val languageLevel = LanguageLevel.forElement(file)
val generatedStatement = generator.createFromImportStatement(languageLevel, path, imported, null)
val formattedStatement = CodeStyleManager.getInstance(file.project).reformat(generatedStatement)
statement.replace(formattedStatement)
}
fun findStatement(file: PsiFile, editor: Editor): PyFromImportStatement? {
val position = file.findElementAt(editor.caretModel.offset)
return PsiTreeUtil.getParentOfType(position, PyFromImportStatement::class.java, true, PyStatement::class.java)
}
}
@@ -0,0 +1,31 @@
// 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.codeInsight.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.jetbrains.extensions.getQName
import com.jetbrains.python.psi.PyFile
/**
* Converts location of 'from .three import foo' to absolute path, e.g. 'from one.two.three import foo'.
*
* @see PyRelativeToAbsoluteImportIntention
* @author Aleksei.Kniazev
*/
class PyRelativeToAbsoluteImportIntention : PyConvertImportIntentionAction("INTN.convert.relative.to.absolute") {
override fun doInvoke(project: Project, editor: Editor, file: PsiFile) {
val statement = findStatement(file, editor) ?: return
val source = statement.resolveImportSource() ?: return
val qName = source.getQName()
replaceImportStatement(statement, file, qName.toString())
}
override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean {
if (file !is PyFile) return false
val statement = findStatement(file, editor) ?: return false
return statement.relativeLevel > 0
}
}
@@ -0,0 +1 @@
from .three import foo
@@ -0,0 +1 @@
from one.two.t<caret>hree import foo
@@ -0,0 +1 @@
from foo.ba<caret>r import baz
@@ -0,0 +1 @@
from .lib import foo
@@ -0,0 +1 @@
from pkg.li<caret>b import foo
@@ -0,0 +1,2 @@
def foo():
pass
@@ -0,0 +1,8 @@
def foo():
pass
def bar():
pass
def baz():
pass
@@ -0,0 +1,3 @@
from . import foo, \
bar, \
baz
@@ -0,0 +1 @@
from one.t<caret>wo import foo, bar, baz
@@ -0,0 +1 @@
from collec<caret>tions import OrderedDict
@@ -0,0 +1,8 @@
def foo():
pass
def bar():
pass
def baz():
pass
@@ -0,0 +1,5 @@
from . import (
foo,
bar,
baz,
)
@@ -0,0 +1,5 @@
from one.t<caret>wo import (
foo,
bar,
baz,
)
@@ -0,0 +1 @@
from one.two.three import foo
@@ -0,0 +1 @@
from .t<caret>hree import foo
@@ -0,0 +1 @@
from pkg.lib import foo
@@ -0,0 +1 @@
from .li<caret>b import foo
@@ -0,0 +1,2 @@
def foo():
pass
@@ -0,0 +1,66 @@
// 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.intentions
import com.intellij.psi.codeStyle.CommonCodeStyleSettings
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonTestUtil
class PyConvertImportIntentionTest : PyIntentionTestCase() {
override fun getTestDataPath(): String = PythonTestUtil.getTestDataPath() + "/intentions/convertImport"
// PY-37858
fun testAbsoluteToRelative() = doTestWithMultipleFiles(PyBundle.message("INTN.convert.absolute.to.relative"))
// PY-37858
fun testRelativeToAbsolute() = doTestWithMultipleFiles(PyBundle.message("INTN.convert.relative.to.absolute"))
// PY-37858
fun testAbsoluteToRelativeInInitFile() = doTestWithMultipleFiles(PyBundle.message("INTN.convert.absolute.to.relative"), "pkg/__init__")
// PY-37858
fun testRelativeToAbsoluteInInitFile() = doTestWithMultipleFiles(PyBundle.message("INTN.convert.relative.to.absolute"), "pkg/__init__")
// PY-37858
fun testPreservingCodestyle() {
pythonCodeStyleSettings.FROM_IMPORT_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS
pythonCodeStyleSettings.FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = true
pythonCodeStyleSettings.FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = true
pythonCodeStyleSettings.FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = true
pythonCodeStyleSettings.FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true
doTestWithMultipleFiles(PyBundle.message("INTN.convert.absolute.to.relative"))
}
// PY-37858
fun testForcingCodestyleIfCurrentIsDifferent() {
pythonCodeStyleSettings.FROM_IMPORT_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS
doTestWithMultipleFiles(PyBundle.message("INTN.convert.absolute.to.relative"))
}
// PY-37858
fun testAbsoluteToRelativeForNonOverlappingLocations() {
val root = getTestName(true)
myFixture.copyDirectoryToProject(root, "")
val file = myFixture.configureByFile("pkg/test.py")
val intentionActions = myFixture.filterAvailableIntentions(PyBundle.message("INTN.convert.absolute.to.relative"))
assertEmpty(intentionActions)
assertSdkRootsNotParsed(file)
}
// PY-37858
fun testNoConvertingStdLibToRelative() = doNegativeTest(PyBundle.message("INTN.convert.absolute.to.relative"))
private fun doTestWithMultipleFiles(hint: String, file: String = "one/two/test") {
val root = getTestName(true)
myFixture.copyDirectoryToProject(root, "")
myFixture.configureByFile("$file.py")
val intentionAction = myFixture.findSingleIntention(hint)
assertNotNull(intentionAction)
myFixture.launchAction(intentionAction)
myFixture.checkResultByFile("$root/$file.after.py", true)
}
}