From 2fcfcacdb7feb8bec3a0f7f181dc63edeff439a9 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 24 Oct 2016 20:37:23 +0300 Subject: [PATCH] PY-21161 Convert to f-string literal for printf-style formatting --- .../after.py.template | 2 + .../before.py.template | 2 + .../description.html | 7 + python/src/META-INF/python-core-common.xml | 5 + .../com/jetbrains/python/PyBundle.properties | 4 + ...ythonFormattedStringReferenceProvider.java | 12 +- ...onvertFormatOperatorToMethodIntention.java | 36 ++- .../PyConvertToFStringIntention.java | 303 ++++++++++++++++++ .../formatMethodByteString.py | 1 + .../formatMethodGluedString.py | 1 + .../percentOperatorDynamicPrecision.py | 1 + .../percentOperatorDynamicWidth.py | 1 + ...rcentOperatorExpressionContainBadQuotes.py | 1 + .../percentOperatorExpressionWithBackslash.py | 1 + .../percentOperatorMultilineExpression.py | 2 + ...tOperatorQuotesInsideInlinedExpressions.py | 5 + ...torQuotesInsideInlinedExpressions_after.py | 1 + ...ercentOperatorSimpleDictConstructorCall.py | 1 + ...OperatorSimpleDictConstructorCall_after.py | 1 + .../percentOperatorSimpleDictLiteral.py | 1 + .../percentOperatorSimpleDictLiteral_after.py | 1 + .../percentOperatorSimpleTuple.py | 1 + .../percentOperatorSimpleTuple_after.py | 1 + .../percentOperatorSingleExpression.py | 1 + .../percentOperatorSingleExpression_after.py | 1 + .../PyConvertToFStringIntentionTest.java | 81 +++++ 26 files changed, 457 insertions(+), 17 deletions(-) create mode 100644 python/resources/intentionDescriptions/PyConvertToFStringIntention/after.py.template create mode 100644 python/resources/intentionDescriptions/PyConvertToFStringIntention/before.py.template create mode 100644 python/resources/intentionDescriptions/PyConvertToFStringIntention/description.html create mode 100644 python/src/com/jetbrains/python/codeInsight/intentions/PyConvertToFStringIntention.java create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodByteString.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodGluedString.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicPrecision.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicWidth.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionContainBadQuotes.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionWithBackslash.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorMultilineExpression.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions_after.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall_after.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral_after.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple_after.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression.py create mode 100644 python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression_after.py create mode 100644 python/testSrc/com/jetbrains/python/intentions/PyConvertToFStringIntentionTest.java diff --git a/python/resources/intentionDescriptions/PyConvertToFStringIntention/after.py.template b/python/resources/intentionDescriptions/PyConvertToFStringIntention/after.py.template new file mode 100644 index 000000000000..01caf319426d --- /dev/null +++ b/python/resources/intentionDescriptions/PyConvertToFStringIntention/after.py.template @@ -0,0 +1,2 @@ +f"Pi is {math.pi:03.5}" +f'{"eggs"}' \ No newline at end of file diff --git a/python/resources/intentionDescriptions/PyConvertToFStringIntention/before.py.template b/python/resources/intentionDescriptions/PyConvertToFStringIntention/before.py.template new file mode 100644 index 000000000000..3815d98cac7f --- /dev/null +++ b/python/resources/intentionDescriptions/PyConvertToFStringIntention/before.py.template @@ -0,0 +1,2 @@ +"Pi is {:03.5}".format(math.pi) +'%(spam)s' % {'spam': 'eggs'} \ No newline at end of file diff --git a/python/resources/intentionDescriptions/PyConvertToFStringIntention/description.html b/python/resources/intentionDescriptions/PyConvertToFStringIntention/description.html new file mode 100644 index 000000000000..e4f408edb7ee --- /dev/null +++ b/python/resources/intentionDescriptions/PyConvertToFStringIntention/description.html @@ -0,0 +1,7 @@ + + +This intention converts string formatting via format() method and format +operator to Python 3.6 f-string literals. + + + \ No newline at end of file diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 939affe7f49e..f1da49fc8607 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -310,6 +310,11 @@ com.jetbrains.python.codeInsight.intentions.PyYieldFromIntention Python + + + com.jetbrains.python.codeInsight.intentions.PyConvertToFStringIntention + Python + com.jetbrains.python.codeInsight.intentions.PyConvertTypeCommentToVariableAnnotationIntention diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 2053d60c3710..7fa68f7a9f64 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -219,7 +219,11 @@ INTN.string.concatenation.to.format=Replace string concatenation with format ope INTN.replace.plus.with.format.operator=Replace + with string formatting operator INTN.replace.plus.with.str.format=Replace + with str.format method call +# ConvertFormatOperatorToMethodIntention INTN.format.operator.to.method=Convert format operator usage to str.format method call + +# PyConvertToFStringIntention +INTN.convert.to.fstring.literal=Convert to f-string literal INTN.replace.with.method=Replace with str.format method call INTN.flip.comparison=Flip comparison diff --git a/python/src/com/jetbrains/python/codeInsight/PythonFormattedStringReferenceProvider.java b/python/src/com/jetbrains/python/codeInsight/PythonFormattedStringReferenceProvider.java index 7d1992225355..dfbfb2c7dd1e 100644 --- a/python/src/com/jetbrains/python/codeInsight/PythonFormattedStringReferenceProvider.java +++ b/python/src/com/jetbrains/python/codeInsight/PythonFormattedStringReferenceProvider.java @@ -37,23 +37,23 @@ public class PythonFormattedStringReferenceProvider extends PsiReferenceProvider } } - private static PsiReference[] getReferencesFromFormatString(@NotNull final PyStringLiteralExpression element) { + private static PySubstitutionChunkReference[] getReferencesFromFormatString(@NotNull final PyStringLiteralExpression element) { final List chunks = PyStringFormatParser.filterSubstitutions( PyStringFormatParser.parseNewStyleFormat(element.getStringValue())); return getReferencesFromChunks(element, chunks, false); } - private static PsiReference[] getReferencesFromPercentString(@NotNull final PyStringLiteralExpression element) { + private static PySubstitutionChunkReference[] getReferencesFromPercentString(@NotNull final PyStringLiteralExpression element) { final List chunks = PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parsePercentFormat(element.getStringValue())); return getReferencesFromChunks(element, chunks, true); } @NotNull - private static PsiReference[] getReferencesFromChunks(@NotNull final PyStringLiteralExpression element, - @NotNull final List chunks, - boolean isPercent) { - final PsiReference[] result = new PsiReference[chunks.size()]; + public static PySubstitutionChunkReference[] getReferencesFromChunks(@NotNull final PyStringLiteralExpression element, + @NotNull final List chunks, + boolean isPercent) { + final PySubstitutionChunkReference[] result = new PySubstitutionChunkReference[chunks.size()]; for (int i = 0; i < chunks.size(); i++) { final PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i); result[i] = new PySubstitutionChunkReference(element, chunk, i, isPercent); diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/ConvertFormatOperatorToMethodIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/ConvertFormatOperatorToMethodIntention.java index dc67148d6fe0..dbb88b323980 100644 --- a/python/src/com/jetbrains/python/codeInsight/intentions/ConvertFormatOperatorToMethodIntention.java +++ b/python/src/com/jetbrains/python/codeInsight/intentions/ConvertFormatOperatorToMethodIntention.java @@ -36,6 +36,7 @@ import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; import com.jetbrains.python.psi.types.*; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -148,17 +149,7 @@ public class ConvertFormatOperatorToMethodIntention extends PyBaseIntentionActio // don't convert %s -> !s, for %s is the normal way to output the default representation out.append(":"); if (f_modifier != null) { - // in strict order - if (has(f_modifier, '-')) out.append("<"); // left align - else if ("s".equals(fConversion) && !StringUtil.isEmptyOrSpaces(f_width)) { - // "%20s" aligns right, "{0:20s}" aligns left; to preserve align, make it explicit - out.append(">"); - } - if (has(f_modifier, '+')) out.append("+"); // signed - else if (has(f_modifier, ' ')) out.append(" "); // default-signed - if (has(f_modifier, '#')) out.append("#"); // alt numbers - if (has(f_modifier, '0')) out.append("0"); // padding - // anything else can't be here + out.append(convertFormatSpec(f_modifier, f_width, fConversion)); } if (f_width != null) { out.append(f_width); @@ -209,6 +200,29 @@ public class ConvertFormatOperatorToMethodIntention extends PyBaseIntentionActio return new Pair<>(result, usesNamedFormat); } + @NotNull + public static String convertFormatSpec(@NotNull String modifier, + @Nullable String widthAndPrecision, + @Nullable String conversionChar) { + final StringBuilder result = new StringBuilder(); + // in strict order + if (has(modifier, '-')) { + result.append("<"); // left align + } + else if ("s".equals(conversionChar) && !StringUtil.isEmptyOrSpaces(widthAndPrecision)) { + // "%20s" aligns right, "{0:20s}" aligns left; to preserve align, make it explicit + result.append(">"); + } + if (has(modifier, '+')) { + result.append("+"); // signed + } + else if (has(modifier, ' ')) result.append(" "); // default-signed + if (has(modifier, '#')) result.append("#"); // alt numbers + if (has(modifier, '0')) result.append("0"); // padding + // anything else can't be here + return result.toString(); + } + private static boolean has(String where, char what) { return where.indexOf(what) >= 0; } diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertToFStringIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertToFStringIntention.java new file mode 100644 index 000000000000..ef20e165cd6a --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertToFStringIntention.java @@ -0,0 +1,303 @@ +/* + * Copyright 2000-2016 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.codeInsight.intentions; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.codeInsight.PySubstitutionChunkReference; +import com.jetbrains.python.codeInsight.PythonFormattedStringReferenceProvider; +import com.jetbrains.python.inspections.PyStringFormatParser; +import com.jetbrains.python.inspections.PyStringFormatParser.ConstantChunk; +import com.jetbrains.python.inspections.PyStringFormatParser.FormatStringChunk; +import com.jetbrains.python.inspections.PyStringFormatParser.SubstitutionChunk; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.PyUtil.StringNodeInfo; +import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static com.jetbrains.python.psi.PyUtil.as; + +/** + * @author Mikhail Golubev + */ +public class PyConvertToFStringIntention extends PyBaseIntentionAction { + @Nls + @NotNull + @Override + public String getFamilyName() { + return PyBundle.message("INTN.convert.to.fstring.literal"); + } + + @NotNull + @Override + public String getText() { + return getFamilyName(); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + if (!(file instanceof PyFile) || !LanguageLevel.forElement(file).isAtLeast(LanguageLevel.PYTHON36)) return false; + + final Pair pair = findTargetStringUnderCaret(editor, file); + if (pair == null) return false; + + final PyStringLiteralExpression pyString = pair.getFirst(); + final boolean percentOperator = pair.getSecond(); + + // TODO handle "glued" literals + if (pyString != null && pyString.getStringNodes().size() == 1) { + final String stringText = pyString.getText(); + final String prefix = PyStringLiteralUtil.getPrefix(stringText); + if (PyStringLiteralUtil.isBytesPrefix(prefix) || PyStringLiteralUtil.isFormattedPrefix(prefix)) { + return false; + } + + final List chunks = percentOperator ? PyStringFormatParser.parsePercentFormat(stringText) + : PyStringFormatParser.parseNewStyleFormat(stringText); + final List substitutions = PyStringFormatParser.filterSubstitutions(chunks); + + // TODO handle dynamic format spec in both formatting styles + final boolean hasDynamicFormatting; + if (percentOperator) { + hasDynamicFormatting = substitutions.stream().anyMatch(s -> "*".equals(s.getWidth()) || "*".equals(s.getPrecision())); + } + else { + hasDynamicFormatting = false; + } + if (hasDynamicFormatting) return false; + + final PySubstitutionChunkReference[] references = + PythonFormattedStringReferenceProvider.getReferencesFromChunks(pyString, substitutions, percentOperator); + + final PsiElement valuesSource; + if (percentOperator) { + final PyBinaryExpression binaryExpression = as(pyString.getParent(), PyBinaryExpression.class); + assert binaryExpression != null; + valuesSource = binaryExpression.getRightExpression(); + } + else { + final PyCallExpression callExpression = PsiTreeUtil.getParentOfType(pyString, PyCallExpression.class); + assert callExpression != null; + valuesSource = callExpression.getArgumentList(); + } + + return Arrays.stream(references) + .map(PyConvertToFStringIntention::getActualReplacementExpression) + .allMatch(element -> element != null && + !(element instanceof PyStarExpression) && + !(element instanceof PyStarArgument) && + PsiTreeUtil.isAncestor(valuesSource, element, false) && + expressionCanBeInlined(pyString, element)); + } + return false; + } + + private static boolean expressionCanBeInlined(@NotNull PyStringLiteralExpression host, @NotNull PyExpression target) { + // Cannot inline multi-line expressions or expressions that contains backslashes (yet) + if (target.textContains('\\') || target.textContains('\n')) return false; + return adjustQuotesInside((PyExpression)target.copy(), host) != null; + } + + @Nullable + private static PsiElement adjustQuotesInside(@NotNull PyExpression element, @NotNull PyStringLiteralExpression host) { + final StringNodeInfo hostInfo = new StringNodeInfo(host.getStringNodes().get(0)); + final char hostQuote = hostInfo.getSingleQuote(); + final PyElementGenerator generator = PyElementGenerator.getInstance(host.getProject()); + + final Collection innerStrings = PsiTreeUtil.collectElementsOfType(element, PyStringLiteralExpression.class); + for (PyStringLiteralExpression literal : innerStrings) { + final List nodes = literal.getStringNodes(); + // TODO figure out what to do with those + if (nodes.size() > 1) { + return null; + } + final StringNodeInfo info = new StringNodeInfo(nodes.get(0)); + // Nest string contain the same type of quote as host string inside, and we cannot escape inside f-string -- retreat + final String content = info.getContent(); + if (content.indexOf(hostQuote) >= 0) { + return null; + } + if (!info.isTerminated()) { + return null; + } + if (info.getSingleQuote() == hostQuote) { + final char targetSingleQuote = hostQuote == '"' ? '\'' : '"'; + final String targetQuote = info.getQuote().replace(hostQuote, targetSingleQuote); + final String stringWithSwappedQuotes = info.getPrefix() + targetQuote + content + targetQuote; + final PsiElement replaced = literal.replace(generator.createStringLiteralAlreadyEscaped(stringWithSwappedQuotes)); + if (literal == element) { + return replaced; + } + } + + } + return element; + } + + @Nullable + private static Pair findTargetStringUnderCaret(@NotNull Editor editor, @NotNull PsiFile file) { + final PsiElement anchor = file.findElementAt(editor.getCaretModel().getOffset()); + if (anchor == null) return null; + + final PyBinaryExpression binaryExpr = PsiTreeUtil.getParentOfType(anchor, PyBinaryExpression.class); + if (binaryExpr != null && binaryExpr.getOperator() == PyTokenTypes.PERC) { + final PyStringLiteralExpression pyString = as(binaryExpr.getLeftExpression(), PyStringLiteralExpression.class); + if (pyString != null) { + return Pair.create(pyString, true); + } + } + final PyCallExpression callExpr = PsiTreeUtil.getParentOfType(anchor, PyCallExpression.class); + if (callExpr != null) { + final PyReferenceExpression callee = as(callExpr.getCallee(), PyReferenceExpression.class); + if (callee != null) { + final PyStringLiteralExpression pyString = as(callee.getQualifier(), PyStringLiteralExpression.class); + if (pyString != null) { + return Pair.create(pyString, false); + } + } + } + return null; + + } + + @Override + public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + final Pair pair = findTargetStringUnderCaret(editor, file); + assert pair != null; + final Boolean percentOperator = pair.getSecond(); + if (percentOperator) { + convertPercentOperatorFormatting(pair.getFirst()); + } + else { + convertFormatMethodFormatting(pair.getFirst()); + } + } + + private static void convertPercentOperatorFormatting(@NotNull PyStringLiteralExpression pyString) { + final String stringText = pyString.getText(); + final Pair quotes = PyStringLiteralUtil.getQuotes(stringText); + assert quotes != null; + final StringBuilder result = new StringBuilder(); + result.append("f"); + result.append(quotes.getFirst().replaceAll("[uU]", "")); + final List chunks = PyStringFormatParser.parsePercentFormat(stringText); + final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(stringText); + int subsChunkPosition = 0; + for (FormatStringChunk chunk : chunks) { + if (chunk instanceof ConstantChunk) { + final TextRange rangeWithoutQuotes = chunk.getTextRange().intersection(contentRange); + assert rangeWithoutQuotes != null; + result.append(rangeWithoutQuotes.substring(stringText)); + } + else { + final SubstitutionChunk subsChunk = (SubstitutionChunk)chunk; + final char conversionChar = subsChunk.getConversionType(); + final String widthAndPrecision; + if (StringUtil.isNotEmpty(subsChunk.getWidth()) || StringUtil.isNotEmpty(subsChunk.getPrecision())) { + widthAndPrecision = StringUtil.notNullize(subsChunk.getWidth()) + "." + StringUtil.notNullize(subsChunk.getPrecision()); + } + else { + widthAndPrecision = ""; + } + final String conversionFlags = subsChunk.getConversionFlags(); + + result.append("{"); + final PySubstitutionChunkReference reference = new PySubstitutionChunkReference(pyString, subsChunk, subsChunkPosition, true); + final PyExpression resolveResult = getActualReplacementExpression(reference); + assert resolveResult != null; + + final PsiElement adjusted = adjustQuotesInside(resolveResult, pyString); + if (adjusted == null) return; + + result.append(adjusted.getText()); + + // TODO mostly duplicates the logic of ConvertFormatOperatorToMethodIntention + if (conversionChar == 'r') { + result.append("!r"); + } + + if ((conversionChar != 'r' && conversionChar != 's') + || StringUtil.isNotEmpty(conversionFlags) + || StringUtil.isNotEmpty(widthAndPrecision)) { + result.append(":"); + } + + if (StringUtil.isNotEmpty(conversionFlags)) { + final String conversionStr = String.valueOf(conversionChar); + result.append(ConvertFormatOperatorToMethodIntention.convertFormatSpec(conversionFlags, widthAndPrecision, conversionStr)); + } + + if (StringUtil.isNotEmpty(widthAndPrecision)) { + result.append(widthAndPrecision); + } + + if ('i' == conversionChar || 'u' == conversionChar) { + result.append("d"); + } + else if ('s' != conversionChar && 'r' != conversionChar) { + result.append(conversionChar); + } + result.append("}"); + subsChunkPosition++; + } + } + result.append(quotes.getSecond()); + + final PyBinaryExpression expressionToReplace = PsiTreeUtil.getParentOfType(pyString, PyBinaryExpression.class); + assert expressionToReplace != null; + + final PyElementGenerator generator = PyElementGenerator.getInstance(pyString.getProject()); + final PyExpression fString = generator.createExpressionFromText(LanguageLevel.PYTHON36, result.toString()); + expressionToReplace.replace(fString); + } + + @Nullable + private static PyExpression getActualReplacementExpression(@NotNull PySubstitutionChunkReference reference) { + final PsiElement resolveResult = reference.resolve(); + if (resolveResult == null) { + return null; + } + final PyKeywordArgument argument = as(resolveResult, PyKeywordArgument.class); + if (argument != null) { + return argument.getValueExpression(); + } + final PyKeyValueExpression parent = as(resolveResult.getParent(), PyKeyValueExpression.class); + if (parent != null && parent.getKey() == resolveResult) { + return parent.getValue(); + } + return as(resolveResult, PyExpression.class); + } + + private void convertFormatMethodFormatting(@NotNull PyStringLiteralExpression pyString) { + + } +} diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodByteString.py b/python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodByteString.py new file mode 100644 index 000000000000..ab8d6cbe96df --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodByteString.py @@ -0,0 +1 @@ +b'{}'.format(42) \ No newline at end of file diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodGluedString.py b/python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodGluedString.py new file mode 100644 index 000000000000..1879e62673ff --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/formatMethodGluedString.py @@ -0,0 +1 @@ +'{}' '{}'.format('foo', 42) diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicPrecision.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicPrecision.py new file mode 100644 index 000000000000..8360174be68f --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicPrecision.py @@ -0,0 +1 @@ +"%.*s" % (5, 'spam') diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicWidth.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicWidth.py new file mode 100644 index 000000000000..c7faa7a440a5 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorDynamicWidth.py @@ -0,0 +1 @@ +"%*s" % (5, 'spam') diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionContainBadQuotes.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionContainBadQuotes.py new file mode 100644 index 000000000000..ada4c8c33ac2 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionContainBadQuotes.py @@ -0,0 +1 @@ +'%s' % "'" \ No newline at end of file diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionWithBackslash.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionWithBackslash.py new file mode 100644 index 000000000000..53ee7d65cdff --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorExpressionWithBackslash.py @@ -0,0 +1 @@ +'%s' % '\n' \ No newline at end of file diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorMultilineExpression.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorMultilineExpression.py new file mode 100644 index 000000000000..67290fc03f38 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorMultilineExpression.py @@ -0,0 +1,2 @@ +'%s' % func(1, + 2) \ No newline at end of file diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions.py new file mode 100644 index 000000000000..b621e8a40bd4 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions.py @@ -0,0 +1,5 @@ +'%s %s %s' % ( + d[r'foo'][b'bar']['baz'], + u'''[]''', + f('foo', g(rf'bar')) +) \ No newline at end of file diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions_after.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions_after.py new file mode 100644 index 000000000000..bb2d34b45147 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorQuotesInsideInlinedExpressions_after.py @@ -0,0 +1 @@ +f'{d[r"foo"][b"bar"]["baz"]} {u"""[]"""} {f("foo", g(rf"bar"))}' \ No newline at end of file diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall.py new file mode 100644 index 000000000000..0179da4dfb1f --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall.py @@ -0,0 +1 @@ +'%(foo)d %(bar)s' % dict(foo=.25, bar='spam'.upper()) diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall_after.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall_after.py new file mode 100644 index 000000000000..6c5ab7eed49c --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictConstructorCall_after.py @@ -0,0 +1 @@ +f'{.25:d} {"spam".upper()}' diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral.py new file mode 100644 index 000000000000..efd21af8cd3a --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral.py @@ -0,0 +1 @@ +"%(foo)s %(bar)r" % {'foo': 'bar', 'bar': [1, 2, 3]} diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral_after.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral_after.py new file mode 100644 index 000000000000..cee61a163f0d --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleDictLiteral_after.py @@ -0,0 +1 @@ +f"{'bar'} {[1, 2, 3]!r}" diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple.py new file mode 100644 index 000000000000..7b80ae7fbbb3 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple.py @@ -0,0 +1 @@ +u'%s %d %f' % ("foo", 42, 2.5) diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple_after.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple_after.py new file mode 100644 index 000000000000..ffaf8ac9b1bf --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSimpleTuple_after.py @@ -0,0 +1 @@ +f'{"foo"} {42:d} {2.5:f}' diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression.py new file mode 100644 index 000000000000..f6061634c60d --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression.py @@ -0,0 +1 @@ +u'%04.5r' % 42 diff --git a/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression_after.py b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression_after.py new file mode 100644 index 000000000000..48c6db99c125 --- /dev/null +++ b/python/testData/intentions/PyConvertToFStringIntentionTest/percentOperatorSingleExpression_after.py @@ -0,0 +1 @@ +f'{42!r:04.5}' diff --git a/python/testSrc/com/jetbrains/python/intentions/PyConvertToFStringIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyConvertToFStringIntentionTest.java new file mode 100644 index 000000000000..11e3946ca1ad --- /dev/null +++ b/python/testSrc/com/jetbrains/python/intentions/PyConvertToFStringIntentionTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2016 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.intentions; + +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.psi.LanguageLevel; + +/** + * @author Mikhail Golubev + */ +public class PyConvertToFStringIntentionTest extends PyIntentionTestCase { + + private void doTest() { + doTest(PyBundle.message("INTN.convert.to.fstring.literal"), LanguageLevel.PYTHON36); + } + + private void doNegativeTest() { + runWithLanguageLevel(LanguageLevel.PYTHON36, () -> doNegativeTest(PyBundle.message("INTN.convert.to.fstring.literal"))); + } + + public void testFormatMethodByteString() { + doNegativeTest(); + } + + public void testFormatMethodGluedString() { + doNegativeTest(); + } + + public void testPercentOperatorDynamicWidth() { + doNegativeTest(); + } + + public void testPercentOperatorDynamicPrecision() { + doNegativeTest(); + } + + public void testPercentOperatorSingleExpression() { + doTest(); + } + + public void testPercentOperatorSimpleTuple() { + doTest(); + } + + public void testPercentOperatorSimpleDictLiteral() { + doTest(); + } + + public void testPercentOperatorSimpleDictConstructorCall() { + doTest(); + } + + public void testPercentOperatorQuotesInsideInlinedExpressions() { + doTest(); + } + + public void testPercentOperatorMultilineExpression() { + doNegativeTest(); + } + + public void testPercentOperatorExpressionWithBackslash() { + doNegativeTest(); + } + + public void testPercentOperatorExpressionContainBadQuotes() { + doNegativeTest(); + } +}