PY-80844 Add a new intention for converting between f-strings and t-strings

GitOrigin-RevId: be3ab4d72de05a92c986af31ac8e40309d9754b1
This commit is contained in:
Mikhail Golubev
2025-05-02 12:08:57 +00:00
committed by intellij-monorepo-bot
parent b8c67616f1
commit f41aae4760
21 changed files with 147 additions and 2 deletions
@@ -1,4 +1,4 @@
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
<idea-plugin>
<dependencies>
<module name="intellij.python.psi"/>
</dependencies>
@@ -387,6 +387,12 @@
<categoryKey>INTN.category.python</categoryKey>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.PyFStringToTStringIntention</className>
<bundleName>messages.PyPsiBundle</bundleName>
<categoryKey>INTN.category.python</categoryKey>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.PyConvertLambdaToFunctionIntention</className>
<bundleName>messages.PyPsiBundle</bundleName>
@@ -603,4 +609,4 @@
<pythonHelpersLocator implementation="com.jetbrains.python.PythonHelpersLocatorDefault" />
</extensions>
</idea-plugin>
</idea-plugin>
@@ -0,0 +1 @@
t"""<span">{content}</span>"""
@@ -0,0 +1 @@
f"""<span">{content}</span>"""
@@ -0,0 +1,7 @@
<html>
<body>
<span>
Converts between f-strings and t-strings (<a href="https://peps.python.org/pep-0750/">PEP-750</a>).
</span>
</body>
</html>
@@ -342,6 +342,11 @@ INTN.convert.absolute.to.relative=Convert absolute import to relative
#PyInvertIfConditionIntention
INTN.invert.if.condition=Invert 'if' condition
# PyFStringToTStringIntention
INTN.NAME.convert.between.f.string.t.string=Convert between f-string and t-string
INTN.convert.f.string.to.t.string=Convert f-string to t-string
INTN.convert.t.string.to.f.string=Convert t-string to f-string
### Quick-fixes ###
QFIX.add.qualifier=Add qualifier
@@ -0,0 +1,48 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.codeInsight.intentions
import com.intellij.modcommand.ActionContext
import com.intellij.modcommand.ModPsiUpdater
import com.intellij.modcommand.Presentation
import com.intellij.modcommand.PsiUpdateModCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.psi.util.parentOfType
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.psi.PyDocStringOwner
import com.jetbrains.python.psi.PyFormattedStringElement
import com.jetbrains.python.psi.PyStringLiteralExpression
import com.jetbrains.python.psi.PyUtil
/**
* Intention to convert between f-strings and t-strings (PEP-750)
*/
class PyFStringToTStringIntention : PsiUpdateModCommandAction<PyFormattedStringElement>(PyFormattedStringElement::class.java) {
override fun getPresentation(context: ActionContext, stringElement: PyFormattedStringElement): Presentation? {
val stringLiteral = stringElement.getParent() as? PyStringLiteralExpression ?: return null
val docStringOwner = stringLiteral.parentOfType<PyDocStringOwner>()
if (docStringOwner != null && docStringOwner.getDocStringExpression() === stringLiteral) return null
if (stringElement.isFormatted) {
return Presentation.of(PyPsiBundle.message("INTN.convert.f.string.to.t.string"))
}
else if (stringElement.isTemplate) {
return Presentation.of(PyPsiBundle.message("INTN.convert.t.string.to.f.string"))
}
return null
}
override fun getFamilyName(): String = PyPsiBundle.message("INTN.NAME.convert.between.f.string.t.string")
override fun invoke(context: ActionContext, stringElement: PyFormattedStringElement, updater: ModPsiUpdater) {
val prefixOffset = stringElement.textOffset
val oldPrefix = stringElement.prefix
val newPrefix = when {
oldPrefix.contains("f") || oldPrefix.contains("F") -> oldPrefix.replace("f", "t").replace("F", "T")
oldPrefix.contains("t") || oldPrefix.contains("T") -> oldPrefix.replace("t", "f").replace("T", "F")
else -> return
}
PyUtil.updateDocumentUnblockedAndCommitted(stringElement) { document: Document ->
document.replaceString(prefixOffset, prefixOffset + oldPrefix.length, newPrefix)
}
}
}
@@ -0,0 +1,4 @@
class MyClass:
f"""This <caret>is a docstring with an f-prefix.
It should not be convertible to a t-string because it's a docstring.
"""
@@ -0,0 +1 @@
s = t"This is a f-string with an {f"emb<caret>edded {value} f-string"}"
@@ -0,0 +1 @@
s = t"This is a f-string with an {t"emb<caret>edded {value} f-string"}"
@@ -0,0 +1,4 @@
s = f"""<caret>
This is a multiline string with {variable1}
and another {variable2} on a different line.
"""
@@ -0,0 +1,4 @@
s = t"""<caret>
This is a multiline string with {variable1}
and another {variable2} on a different line.
"""
@@ -0,0 +1 @@
s = rf"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = rt"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = "<caret>This is a regular string without any prefix"
@@ -0,0 +1 @@
s = f"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = t"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = t"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = f"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = F"Hel<caret>lo, {name}!"
@@ -0,0 +1 @@
s = T"Hel<caret>lo, {name}!"
@@ -0,0 +1,54 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.intentions;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.psi.LanguageLevel;
/**
* Tests for PyFStringToTStringIntention
*/
public class PyFStringToTStringIntentionTest extends PyIntentionTestCase {
public void testSimpleFString() {
doFStringToTStringTest();
}
public void testMultilineFString() {
doFStringToTStringTest();
}
public void testFStringInsideTString() {
doFStringToTStringTest();
}
public void testSimpleTString() {
doTStringToFStringTest();
}
public void testRegularString() {
doNegativeTest();
}
public void testDocString() {
doNegativeTest();
}
public void testRawFString() {
doFStringToTStringTest();
}
public void testUppercaseFStringPrefix() {
doFStringToTStringTest();
}
private void doFStringToTStringTest() {
doTest(PyPsiBundle.message("INTN.convert.f.string.to.t.string"), LanguageLevel.getLatest());
}
private void doTStringToFStringTest() {
doTest(PyPsiBundle.message("INTN.convert.t.string.to.f.string"), LanguageLevel.getLatest());
}
private void doNegativeTest() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> doNegativeTest(PyPsiBundle.message("INTN.convert.f.string.to.t.string")));
}
}