PY-16747, PY-16770 generated abstract methods preserve formatting and prefix of original docstring

This commit is contained in:
Mikhail Golubev
2015-09-01 12:25:08 +03:00
committed by Mikhail Golubev
parent 9213f503e3
commit fc8ea56374
17 changed files with 239 additions and 138 deletions

View File

@@ -67,8 +67,7 @@ public class PyGenerateDocstringIntention extends BaseIntentionAction {
private boolean isAvailableForFunction(PyFunction function) {
if (function.getDocStringValue() != null) {
final PyDocstringGenerator docstringGenerator = PyDocstringGenerator.forDocStringOwner(function);
if (docstringGenerator.hasParametersToAdd()) {
if (PyDocstringGenerator.forDocStringOwner(function).withInferredParameters(false).hasParametersToAdd()) {
myText = PyBundle.message("INTN.add.parameters.to.docstring");
return true;
}
@@ -101,8 +100,10 @@ public class PyGenerateDocstringIntention extends BaseIntentionAction {
if (!DocStringUtil.ensureNotPlainDocstringFormat(docStringOwner)) {
return;
}
final PyDocstringGenerator docstringGenerator = PyDocstringGenerator.forDocStringOwner(docStringOwner);
docstringGenerator.addFirstEmptyLine();
final PyDocstringGenerator docstringGenerator = PyDocstringGenerator
.forDocStringOwner(docStringOwner)
.withInferredParameters(false)
.addFirstEmptyLine();
final PyStringLiteralExpression updated = docstringGenerator.buildAndInsert().getDocStringExpression();
if (updated != null && editor != null) {
final int offset = updated.getTextOffset();

View File

@@ -79,7 +79,6 @@ public class SpecifyTypeInDocstringIntention extends TypeIntention {
final boolean isReturn = "rtype".equals(kind);
final PyDocstringGenerator docstringGenerator = PyDocstringGenerator.forDocStringOwner(pyFunction);
docstringGenerator.addFirstEmptyLine();
final PySignature signature = PySignatureCacheManager.getInstance(pyFunction.getProject()).findSignature(pyFunction);
final String name = isReturn ? "" : StringUtil.notNullize(problemElement.getName());
final String type;
@@ -96,7 +95,7 @@ public class SpecifyTypeInDocstringIntention extends TypeIntention {
docstringGenerator.withReturnValue(type);
}
docstringGenerator.buildAndInsert();
docstringGenerator.addFirstEmptyLine().buildAndInsert();
docstringGenerator.startTemplate();
}

View File

@@ -64,25 +64,24 @@ public class PyDocstringGenerator {
// Updated after buildAndInsert()
@Nullable private PyDocStringOwner myDocStringOwner;
private final String myDocStringIndent;
private final DocStringFormat myDocStringFormat;
private String myDocStringIndent;
private DocStringFormat myDocStringFormat;
private boolean myUseTypesFromDebuggerSignature = true;
private boolean myNewMode = false; // true - generate new string, false - update existing
private boolean myAddFirstEmptyLine = false;
private boolean myParametersPrepared = false;
private boolean myAlwaysGenerateReturn;
private String myQuotes = TRIPLE_DOUBLE_QUOTES;
private PyDocstringGenerator(@Nullable PyDocStringOwner docStringOwner,
@NotNull DocStringFormat format,
@NotNull String indentation,
@NotNull String docStringText) {
@Nullable String docStringText,
@NotNull DocStringFormat format,
@NotNull String indentation) {
myDocStringOwner = docStringOwner;
myDocStringIndent = indentation;
myDocStringFormat = format;
myDocStringText = docStringText;
myNewMode = StringUtil.isEmpty(myDocStringText);
myNewMode = myDocStringText == null;
}
@NotNull
@@ -91,27 +90,27 @@ public class PyDocstringGenerator {
if (owner instanceof PyStatementListContainer) {
indentation = PyIndentUtil.getElementIndent(((PyStatementListContainer)owner).getStatementList());
}
final String docStringText = owner.getDocStringExpression() == null ? "" : owner.getDocStringExpression().getText();
return new PyDocstringGenerator(owner, DocStringUtil.getDocStringFormat(owner), indentation, docStringText);
final String docStringText = owner.getDocStringExpression() == null ? null : owner.getDocStringExpression().getText();
return new PyDocstringGenerator(owner, docStringText, DocStringUtil.getDocStringFormat(owner), indentation);
}
@NotNull
public static PyDocstringGenerator create(@NotNull DocStringFormat format, @NotNull String indentation) {
return new PyDocstringGenerator(null, format, indentation, "");
return new PyDocstringGenerator(null, null, format, indentation);
}
@NotNull
public static PyDocstringGenerator update(@NotNull PyStringLiteralExpression docString) {
return new PyDocstringGenerator(PsiTreeUtil.getParentOfType(docString, PyDocStringOwner.class),
DocStringUtil.getDocStringFormat(docString),
PyIndentUtil.getElementIndent(docString), docString.getText());
return new PyDocstringGenerator(PsiTreeUtil.getParentOfType(docString, PyDocStringOwner.class),
docString.getText(), DocStringUtil.getDocStringFormat(docString),
PyIndentUtil.getElementIndent(docString));
}
@NotNull
public static PyDocstringGenerator update(@NotNull DocStringFormat format,
@NotNull String indentation,
@NotNull String text) {
return new PyDocstringGenerator(null, format, indentation, text);
return new PyDocstringGenerator(null, text, format, indentation);
}
@NotNull
@@ -149,16 +148,6 @@ public class PyDocstringGenerator {
return this;
}
/**
* By default return declaration is added only if function body contains return statement. Sometimes it's not possible, e.g.
* in {@link com.jetbrains.python.editor.PythonEnterHandler} where unclosed docstring literal "captures" whole function body
* including return statements.
*/
@NotNull
public PyDocstringGenerator forceAddReturn() {
myAlwaysGenerateReturn = true;
return this;
}
@NotNull
public PyDocstringGenerator addFirstEmptyLine() {
@@ -173,34 +162,65 @@ public class PyDocstringGenerator {
}
/**
* Populate parameters for function if nothing was specified.
* Order parameters, remove duplicates and merge parameters with and without type according to docstring format.
* @param alwaysAddReturn by default return declaration is added only if function body contains return statement. Sometimes it's not
* possible, e.g. in {@link com.jetbrains.python.editor.PythonEnterHandler} where unclosed docstring literal
* "captures" whole function body including return statements.
*/
private void prepareParameters() {
// Populate parameter list, if no one was specified explicitly to either add or remove
if (!myParametersPrepared && myAddedParams.isEmpty() && myRemovedParams.isEmpty()) {
if (myDocStringOwner instanceof PyFunction) {
for (PyParameter param : ((PyFunction)myDocStringOwner).getParameterList().getParameters()) {
final String paramName = param.getName();
final StructuredDocString docString = getStructuredDocString();
if (StringUtil.isEmpty(paramName) || param.isSelf() || docString != null && docString.getParameters().contains(paramName)) {
continue;
}
withParam(paramName);
@NotNull
public PyDocstringGenerator withInferredParameters(boolean alwaysAddReturn) {
if (myDocStringOwner instanceof PyFunction) {
for (PyParameter param : ((PyFunction)myDocStringOwner).getParameterList().getParameters()) {
final String paramName = param.getName();
final StructuredDocString docString = getStructuredDocString();
if (StringUtil.isEmpty(paramName) || param.isSelf() || docString != null && docString.getParameters().contains(paramName)) {
continue;
}
final RaiseVisitor visitor = new RaiseVisitor();
final PyStatementList statementList = ((PyFunction)myDocStringOwner).getStatementList();
statementList.accept(visitor);
if (visitor.myHasReturn || myAlwaysGenerateReturn) {
// will add :return: placeholder in Sphinx/Epydoc docstrings
myAddedParams.add(new DocstringParam("", null, true));
if (PyCodeInsightSettings.getInstance().INSERT_TYPE_DOCSTUB) {
withReturnValue("");
}
withParam(paramName);
}
final RaiseVisitor visitor = new RaiseVisitor();
final PyStatementList statementList = ((PyFunction)myDocStringOwner).getStatementList();
statementList.accept(visitor);
if (visitor.myHasReturn || alwaysAddReturn) {
// will add :return: placeholder in Sphinx/Epydoc docstrings
myAddedParams.add(new DocstringParam("", null, true));
if (PyCodeInsightSettings.getInstance().INSERT_TYPE_DOCSTUB) {
withReturnValue("");
}
}
}
return this;
}
@NotNull
public String getDocStringIndent() {
return myDocStringIndent;
}
public void setDocStringFormat(@NotNull DocStringFormat format) {
myDocStringFormat = format;
}
@NotNull
public DocStringFormat getDocStringFormat() {
return myDocStringFormat;
}
public void setDocStringIndent(@NotNull String docStringIndent) {
myDocStringIndent = docStringIndent;
}
public boolean isNewMode() {
return myNewMode;
}
/**
* Populate parameters for function if nothing was specified.
* Order parameters, remove duplicates and merge parameters with and without type according to docstring format.
*/
private void prepareParameters() {
if (myParametersPrepared) {
return;
}
final Set<Pair<String, Boolean>> withoutType = Sets.newHashSet();
final Map<Pair<String, Boolean>, String> paramTypes = Maps.newHashMap();
for (DocstringParam param : myAddedParams) {
@@ -278,7 +298,7 @@ public class PyDocstringGenerator {
@Nullable
private StructuredDocString getStructuredDocString() {
return myDocStringText.isEmpty() ? null : DocStringUtil.parseDocString(myDocStringFormat, myDocStringText);
return myDocStringText == null ? null : DocStringUtil.parseDocString(myDocStringFormat, myDocStringText);
}
public void startTemplate() {

View File

@@ -29,7 +29,6 @@ import com.intellij.util.xmlb.annotations.Transient;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.psi.StructuredDocString;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -135,21 +134,4 @@ public class PyDocumentationSettings implements PersistentStateComponent<PyDocum
public void loadState(PyDocumentationSettings state) {
XmlSerializerUtil.copyBean(state, this);
}
/**
* TODO: Use this factory for the whole document infrastructure to simplify new documentation engine support
* Factory that returns appropriate instance of {@link StructuredDocString} if specificed
*
* @return instance or null if no doctype os set
*/
@Nullable
public StructuredDocString getDocString() {
if (myDocStringFormat.equals(DocStringFormat.EPYTEXT)) {
return DocStringUtil.parseDocStringContent(DocStringFormat.EPYTEXT, "");
}
if (myDocStringFormat.equals(DocStringFormat.REST)) {
return DocStringUtil.parseDocStringContent(DocStringFormat.REST, "");
}
return null;
}
}

View File

@@ -262,7 +262,7 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter {
final int caretOffset = editor.getCaretModel().getOffset();
final String quotes = editor.getDocument().getText(TextRange.from(caretOffset - 3, 3));
final String docString = PyDocstringGenerator.forDocStringOwner(docOwner)
.forceAddReturn()
.withInferredParameters(true)
.withQuotes(quotes)
.forceNewMode()
.buildDocString();

View File

@@ -51,8 +51,8 @@ public class PythonSpaceHandler extends TypedHandlerDelegate {
final String quotes = document.getText(TextRange.from(expectedStringStart, 3));
final String docString = PyDocstringGenerator.forDocStringOwner(docOwner)
.forceNewMode()
.withInferredParameters(true)
.withQuotes(quotes)
.forceAddReturn()
.buildDocString();
document.insertString(offset, docString.substring(3));
if (!StringUtil.isEmptyOrSpaces(docString.substring(3, docString.length() - 3))) {

View File

@@ -18,31 +18,27 @@ package com.jetbrains.python.psi.impl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.util.ArrayUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.documentation.DocStringFormat;
import com.jetbrains.python.documentation.PyDocstringGenerator;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.regex.Pattern;
/**
* @author yole
*/
public class PyFunctionBuilder {
private static final String COMMENTS_BOUNDARY = "\"\"\"";
private static final Pattern INDENT_REMOVE_PATTERN = Pattern.compile("^\\s+", Pattern.MULTILINE);
private final String myName;
private final List<String> myParameters = new ArrayList<String>();
private final List<String> myStatements = new ArrayList<String>();
private final List<String> myDecorators = new ArrayList<String>();
private String myAnnotation = null;
private String[] myDocStringLines = null;
@NotNull
private final Map<String, String> myDecoratorValues = new HashMap<String, String>();
private PyDocstringGenerator myDocStringGenerator = PyDocstringGenerator.create(DocStringFormat.REST, " ");
/**
* Creates builder copying signature and doc from another one.
@@ -72,35 +68,11 @@ public class PyFunctionBuilder {
}
}
}
final String docString = source.getDocStringValue();
if (docString != null) {
functionBuilder.docString(docString);
}
functionBuilder.myDocStringGenerator = PyDocstringGenerator.forDocStringOwner(source);
return functionBuilder;
}
/**
* Adds docstring to function. Provide doc with out of comment blocks.
*
*
* @param docString doc
*/
public void docString(@NotNull final String docString) {
final String[] stringsToAdd = StringUtil.splitByLines(removeIndent(docString));
if (myDocStringLines == null) {
myDocStringLines = stringsToAdd;
}
else {
myDocStringLines = ArrayUtil.mergeArrays(myDocStringLines, stringsToAdd);
}
}
@NotNull
private static String removeIndent(@NotNull final String string) {
return INDENT_REMOVE_PATTERN.matcher(string).replaceAll("");
}
public PyFunctionBuilder(String name) {
public PyFunctionBuilder(@NotNull String name) {
myName = name;
}
@@ -108,14 +80,15 @@ public class PyFunctionBuilder {
* Adds param and its type to doc
* @param name param name
* @param type param type
* @param docStyle what docstyle to use to doc param type
* @param format what docstyle to use to doc param type
*/
@NotNull
public PyFunctionBuilder parameterWithType(@NotNull final String name,
@NotNull final String type,
@NotNull final StructuredDocString docStyle) {
@NotNull final DocStringFormat format) {
parameter(name);
docString(docStyle.createParameterType(name, type));
myDocStringGenerator.setDocStringFormat(format);
myDocStringGenerator.withParamTypedByName(name, type);
return this;
}
@@ -175,20 +148,15 @@ public class PyFunctionBuilder {
builder.append(":");
List<String> statements = myStatements.isEmpty() ? Collections.singletonList(PyNames.PASS) : myStatements;
if (myDocStringLines != null) {
final List<String> comments = new ArrayList<String>(myDocStringLines.length + 2);
comments.add(COMMENTS_BOUNDARY);
comments.addAll(Arrays.asList(myDocStringLines));
comments.add(COMMENTS_BOUNDARY);
statements = new ArrayList<String>(statements);
statements.addAll(0, comments);
final String indent = PyIndentUtil.getIndentFromSettings(project);
myDocStringGenerator.setDocStringIndent(indent);
// There was original docstring or some parameters were added via parameterWithType()
if (!myDocStringGenerator.isNewMode() || myDocStringGenerator.hasParametersToAdd()) {
final String docstring = PyIndentUtil.changeIndent(myDocStringGenerator.buildDocString(), true, indent);
builder.append('\n').append(indent).append(docstring);
}
final CodeStyleSettings codeStyleSettings = CodeStyleSettingsManager.getInstance(project).getCurrentSettings();
int indentSize = codeStyleSettings.getIndentOptions(PythonFileType.INSTANCE).INDENT_SIZE;
String indent = StringUtil.repeatSymbol(' ', indentSize);
for (String statement : statements) {
builder.append("\n").append(indent).append(statement);
builder.append('\n').append(indent).append(statement);
}
return builder.toString();
}
@@ -207,11 +175,4 @@ public class PyFunctionBuilder {
public void decorate(String decoratorName) {
myDecorators.add(decoratorName);
}
@NotNull
private static String getIndent(@NotNull final Project project) {
final CodeStyleSettings codeStyleSettings = CodeStyleSettingsManager.getInstance(project).getCurrentSettings();
final int indentSize = codeStyleSettings.getIndentOptions(PythonFileType.INSTANCE).INDENT_SIZE;
return StringUtil.repeatSymbol(' ', indentSize);
}
}

View File

@@ -0,0 +1,22 @@
from abc import abstractmethod, ABCMeta
class A:
__metaclass__ = ABCMeta
@abstractmethod
def m(self, x):
"""
Parameters:
x (int): number
"""
pass
class B(A):
def m(self, x):
"""
Parameters:
x (int): number
"""
return x

View File

@@ -0,0 +1,7 @@
class B:
def m(self, x):
"""
Parameters:
x (int): number
"""
return x

View File

@@ -0,0 +1,17 @@
# coding=utf-8
from abc import abstractmethod, ABCMeta
class A:
__metaclass__ = ABCMeta
@abstractmethod
def m(self, x):
u"""Юникод"""
pass
class B(A):
def m(self, x):
u"""Юникод"""
return x

View File

@@ -0,0 +1,6 @@
# coding=utf-8
class B:
def m(self, x):
u"""Юникод"""
return x

View File

@@ -0,0 +1,22 @@
from abc import abstractmethod, ABCMeta
class A:
__metaclass__ = ABCMeta
@abstractmethod
def m(self, x):
"""
Parameters:
x (int): number
"""
pass
class B(A):
def m(self, x):
"""
Parameters:
x (int): number
"""
return x

View File

@@ -0,0 +1,11 @@
class A:
pass
class B(A):
def m(self, x):
"""
Parameters:
x (int): number
"""
return x

View File

@@ -0,0 +1,17 @@
# coding=utf-8
from abc import abstractmethod, ABCMeta
class A:
__metaclass__ = ABCMeta
@abstractmethod
def m(self, x):
u"""Юникод"""
pass
class B(A):
def m(self, x):
u"""Юникод"""
return x

View File

@@ -0,0 +1,10 @@
# coding=utf-8
class A:
pass
class B(A):
def m(self, x):
u"""Юникод"""
return x

View File

@@ -95,25 +95,25 @@ public class PyExtractSuperclassTest extends PyClassRefactoringTest {
}
public void testSimple() throws Exception {
doSimpleTest("Foo", "Suppa", null, true, ".foo");
doSimpleTest("Foo", "Suppa", null, true, false, ".foo");
}
public void testInstanceNotDeclaredInInit() throws Exception {
doSimpleTest("Child", "Parent", null, true, "#eggs");
doSimpleTest("Child", "Parent", null, true, false, "#eggs");
}
public void testWithSuper() throws Exception {
doSimpleTest("Foo", "Suppa", null, true, ".foo");
doSimpleTest("Foo", "Suppa", null, true, false, ".foo");
}
public void testWithImport() throws Exception {
doSimpleTest("A", "Suppa", null, false, ".foo");
doSimpleTest("A", "Suppa", null, false, false, ".foo");
}
// PY-12175
public void testImportNotBroken() throws Exception {
myFixture.copyFileToProject("/refactoring/extractsuperclass/shared.py", "shared.py");
doSimpleTest("Source", "DestClass", null, true, "SharedClass");
doSimpleTest("Source", "DestClass", null, true, false, "SharedClass");
}
// PY-12175 but between several files
@@ -122,19 +122,29 @@ public class PyExtractSuperclassTest extends PyClassRefactoringTest {
}
public void testMoveFields() throws Exception {
doSimpleTest("FromClass", "ToClass", null, true, "#instance_field", "#CLASS_FIELD");
doSimpleTest("FromClass", "ToClass", null, true, false, "#instance_field", "#CLASS_FIELD");
}
public void testProperties() throws Exception {
doSimpleTest("FromClass", "ToClass", null, true, "#C", "#a", "._get", ".foo");
doSimpleTest("FromClass", "ToClass", null, true, false, "#C", "#a", "._get", ".foo");
}
// PY-16747
public void testAbstractMethodDocStringIndentationPreserved() throws Exception {
doSimpleTest("B", "A", null, true, true, ".m");
}
// PY-16770
public void testAbstractMethodDocStringPrefixPreserved() throws Exception {
doSimpleTest("B", "A", null, true, true, ".m");
}
private void doSimpleTest(final String className,
final String superclassName,
final String expectedError,
final boolean sameFile,
final String... membersName) throws Exception {
boolean asAbstract, final String... membersName) throws Exception {
try {
String baseName = "/refactoring/extractsuperclass/" + getTestName(true);
myFixture.configureByFile(baseName + ".before.py");
@@ -142,7 +152,9 @@ public class PyExtractSuperclassTest extends PyClassRefactoringTest {
final List<PyMemberInfo<PyElement>> members = new ArrayList<PyMemberInfo<PyElement>>();
for (String memberName : membersName) {
final PyElement member = findMember(className, memberName);
members.add(MembersManager.findMember(clazz, member));
final PyMemberInfo<PyElement> memberInfo = MembersManager.findMember(clazz, member);
memberInfo.setToAbstract(asAbstract);
members.add(memberInfo);
}
new WriteCommandAction.Simple(myFixture.getProject()) {

View File

@@ -111,6 +111,20 @@ public class PyPullUpTest extends PyClassRefactoringTest {
checkMultiFile(modules);
}
// PY-16747
public void testAbstractMethodDocStringIndentationPreserved() {
myFixture.configureByFile(getMultiFileBaseName() + ".py");
doPullUp("B", "A", true, ".m");
myFixture.checkResultByFile(getMultiFileBaseName() + ".after.py");
}
// PY-16770
public void testAbstractMethodDocStringPrefixPreserved() {
myFixture.configureByFile(getMultiFileBaseName() + ".py");
doPullUp("B", "A", true, ".m");
myFixture.checkResultByFile(getMultiFileBaseName() + ".after.py");
}
private void doMultiFileTest() {
final String[] modules = {"Class", "SuperClass"};
configureMultiFile(modules);