PY-9764 Allow to force trailing comma in multiline "from" import statements

This commit is contained in:
Mikhail Golubev
2017-01-23 17:41:20 +03:00
parent 6f7faffcc3
commit 466af87373
10 changed files with 190 additions and 37 deletions
@@ -1035,6 +1035,7 @@ formatter.align.when.multiline=Align when multiline
formatter.collections.and.comprehensions=Collections and Comprehensions
formatter.from.import.statements="From" Import Statements
formatter.from.import.statements.force.parentheses=Force parentheses
formatter.from.import.statements.force.comma.if.multline=Force trailing comma if multiline
formatter.dictionary.literals=Dictionary literals
formatter.hang.closing.brackets=Hang closing brackets
@@ -120,6 +120,7 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings {
CommonCodeStyleSettings.FORCE_BRACES_ALWAYS}
)
public int FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE;
public boolean FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = false;
/**
* Corresponds to the option of pycodestyle.py "--hang-closing". Basically, it means that the closing brace of a collection literal,
@@ -25,7 +25,10 @@ import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor;
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessorHelper;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -57,14 +60,24 @@ public class PyFromImportPostFormatProcessor implements PostFormatProcessor {
@Override
public void visitPyFromImportStatement(PyFromImportStatement node) {
if (myHelper.isElementFullyInRange(node)) {
final PyImportElement[] importedNames = node.getImportElements();
final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class);
final PsiElement leftParen = node.getLeftParen();
final boolean enabledInSettings = pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_ALWAYS ||
pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE &&
PostFormatProcessorHelper.isMultiline(node);
if (enabledInSettings && importedNames.length > 1 && leftParen == null) {
myImportStatements.add(node);
// If non-parenthesized "from" import ends with one or more of trailing commas, the array returned by getImportElements()
// contains empty import elements at the end
final List<PyImportElement> importedNames = ContainerUtil.filter(node.getImportElements(), elem -> elem.getTextLength() != 0);
if (importedNames.size() > 1) {
final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class);
final boolean forcedParentheses = pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_ALWAYS ||
pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE &&
PostFormatProcessorHelper.isMultiline(node);
final boolean forcedComma = pySettings.FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE && PostFormatProcessorHelper.isMultiline(node);
final PyImportElement lastImportedName = importedNames.get(importedNames.size() - 1);
final PsiElement afterLastName = PyPsiUtils.getNextNonCommentSibling(lastImportedName, true);
final PsiElement openingParen = node.getLeftParen();
final boolean missingComma = afterLastName == null || afterLastName.getNode().getElementType() != PyTokenTypes.COMMA;
// Trailing comma is allowed only in "from" imports wrapped in parentheses
if (forcedParentheses && openingParen == null || forcedComma && missingComma && openingParen != null) {
myImportStatements.add(node);
}
}
}
}
@@ -94,34 +107,55 @@ public class PyFromImportPostFormatProcessor implements PostFormatProcessor {
@NotNull
private PyFromImportStatement replaceFromImport(@NotNull PyFromImportStatement fromImport) {
final PyImportElement firstName = fromImport.getImportElements()[0];
final String beforeFirstName = fromImport.getText().substring(0, firstName.getStartOffsetInParent());
final StringBuilder newStatementText = new StringBuilder(beforeFirstName);
newStatementText.append("(");
boolean lastElementWasComment = false;
for (PsiElement cur = firstName; cur != null; cur = cur.getNextSibling()) {
if (cur instanceof PsiWhiteSpace) {
newStatementText.append(cur.getText().replace("\\", ""));
}
else {
newStatementText.append(cur.getText());
}
lastElementWasComment = cur instanceof PsiComment;
}
if (lastElementWasComment) {
newStatementText.append("\n");
}
newStatementText.append(")");
final PyImportElement[] allNames = fromImport.getImportElements();
final PyImportElement firstName = allNames[0];
final PyElementGenerator generator = PyElementGenerator.getInstance(fromImport.getProject());
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(fromImport.getProject());
final LanguageLevel level = LanguageLevel.forElement(fromImport);
PyFromImportStatement newFromImport = generator.createFromText(level, PyFromImportStatement.class, newStatementText.toString());
newFromImport = (PyFromImportStatement)fromImport.replace(newFromImport);
newFromImport = (PyFromImportStatement)codeStyleManager.reformat(newFromImport);
myHelper.updateResultRange(fromImport.getTextLength(), newFromImport.getTextLength());
return newFromImport;
if (fromImport.getLeftParen() == null) {
// Surround with parentheses stripping obsolete continuation backslashes and added trailing comma if necessary
final String beforeFirstName = fromImport.getText().substring(0, firstName.getStartOffsetInParent());
final StringBuilder newStatementText = new StringBuilder(beforeFirstName);
newStatementText.append("(");
boolean lastElementWasComment = false;
int lastVisibleNameCommaOffset = -1;
for (PsiElement cur = firstName; cur != null; cur = cur.getNextSibling()) {
if (cur instanceof PsiWhiteSpace) {
newStatementText.append(cur.getText().replace("\\", ""));
}
else {
newStatementText.append(cur.getText());
}
if (cur instanceof PyImportElement && cur.getTextLength() != 0) {
lastVisibleNameCommaOffset = newStatementText.length();
}
else if (lastVisibleNameCommaOffset != -1 && cur.getNode().getElementType() == PyTokenTypes.COMMA) {
lastVisibleNameCommaOffset = -1;
}
lastElementWasComment = cur instanceof PsiComment;
}
final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class);
if (lastVisibleNameCommaOffset != -1 && pySettings.FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE) {
newStatementText.insert(lastVisibleNameCommaOffset, ",");
}
if (lastElementWasComment) {
newStatementText.append("\n");
}
newStatementText.append(")");
final LanguageLevel level = LanguageLevel.forElement(fromImport);
PyFromImportStatement newFromImport = generator.createFromText(level, PyFromImportStatement.class, newStatementText.toString());
newFromImport = (PyFromImportStatement)fromImport.replace(newFromImport);
newFromImport = (PyFromImportStatement)codeStyleManager.reformat(newFromImport, true);
myHelper.updateResultRange(fromImport.getTextLength(), newFromImport.getTextLength());
return newFromImport;
}
else {
// Add only trailing comma
final PsiElement comma = fromImport.addAfter(generator.createComma().getPsi(), allNames[allNames.length - 1]);
codeStyleManager.reformat(comma);
return fromImport;
}
}
}
@@ -135,6 +135,9 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin
PyBundle.message("formatter.from.import.statements.force.parentheses"),
PyBundle.message("formatter.from.import.statements"),
BRACE_OPTIONS, BRACE_VALUES);
consumer.showCustomOption(PyCodeStyleSettings.class, "FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE",
PyBundle.message("formatter.from.import.statements.force.comma.if.multline"),
PyBundle.message("formatter.from.import.statements"));
consumer.showCustomOption(PyCodeStyleSettings.class, "DICT_WRAPPING",
PyBundle.message("formatter.dictionary.literals"), null, WRAP_OPTIONS, WRAP_VALUES);
@@ -0,0 +1,23 @@
from module import foo
from module import foo, bar
from module import foo, bar,
# | margin
from module import foo, bar, baz
from module import foo, \
bar
from module import foo, \
bar,
from module import foo, \
bar # comment
from module import (foo,
bar)
from module import (foo,
bar,)
from module import (
foo,
bar # comment
)
@@ -0,0 +1,27 @@
from module import foo
from module import (foo,
bar, )
from module import (foo,
bar, )
# | margin
from module import (foo, bar,
baz, )
from module import (foo,
bar, )
from module import (foo,
bar, )
from module import (foo,
bar, # comment
)
from module import (foo,
bar, )
from module import (foo,
bar, )
from module import (
foo,
bar, # comment
)
@@ -0,0 +1,23 @@
from module import foo
from module import foo, bar
from module import foo, bar,
# | margin
from module import foo, bar, baz
from module import foo, \
bar
from module import foo, \
bar,
from module import foo, \
bar # comment
from module import (foo,
bar)
from module import (foo,
bar,)
from module import (
foo,
bar # comment
)
@@ -0,0 +1,24 @@
from module import foo
from module import foo, bar
from module import foo, bar,
# | margin
from module import foo, bar, \
baz
from module import foo, \
bar
from module import foo, \
bar,
from module import foo, \
bar # comment
from module import (foo,
bar, )
from module import (foo,
bar, )
from module import (
foo,
bar, # comment
)
@@ -1,15 +1,15 @@
from module import foo
from module import (
foo,
bar
bar,
)
from module import (
foo,
bar
bar,
)
from module import (
foo,
bar # comment
bar, # comment
)
from module import (
foo,
@@ -786,10 +786,27 @@ public class PyFormatterTest extends PyTestCase {
getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = true;
getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = true;
getPythonCodeStyleSettings().FROM_IMPORT_WRAPPING = WrapType.ALWAYS.getLegacyRepresentation();
getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true;
getPythonCodeStyleSettings().HANG_CLOSING_BRACKETS = true;
doTest();
}
// PY-9764
public void testFromImportTrailingCommaWithParentheses() {
getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30);
getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS;
getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true;
doTest();
}
// PY-9764
public void testFromImportTrailingCommaWithoutParentheses() {
getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30);
getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE;
getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true;
doTest();
}
public void testVariableAnnotations() {
runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest);
}