PY-9795 First version of parsing of section-based docstrings

This commit is contained in:
Mikhail Golubev
2015-09-02 14:33:52 +03:00
parent b9ed603e75
commit f362f0e6a2
6 changed files with 726 additions and 4 deletions
@@ -17,6 +17,7 @@ package com.jetbrains.python.toolbox;
import com.intellij.openapi.util.TextRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -34,10 +35,26 @@ import java.util.regex.Pattern;
public class Substring implements CharSequence {
private static final Pattern RE_NL = Pattern.compile("(\\r?\\n)");
@NotNull
public static Substring fromMatcherGroup(@NotNull String s, @NotNull Matcher matcher, int groupNumber) {
if (matcher.groupCount() < groupNumber || matcher.end(groupNumber) > s.length()) {
throw new IllegalArgumentException("Inconsistent matcher, group number and underlying string");
}
return new Substring(s, matcher.start(groupNumber), matcher.end(groupNumber));
}
@NotNull
public static Substring fromMatcherGroup(@NotNull Substring s, @NotNull Matcher matcher, int groupNumber) {
if (matcher.groupCount() < groupNumber || matcher.end(groupNumber) > s.length()) {
throw new IllegalArgumentException("Inconsistent matcher, group number and underlying string");
}
return new Substring(s.getSuperString(), s.myStartOffset + matcher.start(groupNumber), s.myStartOffset + matcher.end(groupNumber));
}
@NotNull private final String myString;
private final int myStartOffset;
private final int myEndOffset;
public Substring(@NotNull String s) {
this(s, 0, s.length());
}
@@ -87,21 +104,33 @@ public class Substring implements CharSequence {
@NotNull
public List<Substring> split(@NotNull String regex) {
return split(Pattern.compile(regex));
return split(regex, Integer.MAX_VALUE);
}
@NotNull
public List<Substring> split(@NotNull String regex, int maxSplits) {
return split(Pattern.compile(regex), maxSplits);
}
@NotNull
public List<Substring> split(@NotNull Pattern pattern) {
return split(pattern, Integer.MAX_VALUE);
}
@NotNull
public List<Substring> split(@NotNull Pattern pattern, int maxSplits) {
final List<Substring> result = new ArrayList<Substring>();
final Matcher m = pattern.matcher(myString);
int start = myStartOffset;
int end = myEndOffset;
int splitCount = 0;
if (m.find(start)) {
do {
splitCount++;
end = m.start();
result.add(createAnotherSubstring(start, Math.min(end, myEndOffset)));
start = m.end();
} while (end < myEndOffset && m.find());
} while (end < myEndOffset && m.find() && splitCount < maxSplits);
if (start < myEndOffset) {
result.add(createAnotherSubstring(start, myEndOffset));
}
@@ -190,4 +219,26 @@ public class Substring implements CharSequence {
private Substring createAnotherSubstring(int start, int end) {
return new Substring(myString, start, end);
}
public int getStartOffset() {
return myStartOffset;
}
public int getEndOffset() {
return myEndOffset;
}
/**
* If both substrings share the same origin, returns new substring that includes both of them. Otherwise return {@code null}.
*
* @param other substring to concat with
* @return new substring as described
*/
@Nullable
public Substring getSmallestInclusiveSubstring(@NotNull Substring other) {
if (myString.equals(other.myString)) {
return new Substring(myString, Math.min(myStartOffset, other.myStartOffset), Math.max(myEndOffset, other.myEndOffset));
}
return null;
}
}
@@ -27,8 +27,9 @@ public class DocStringFormat {
public static final String EPYTEXT = "Epytext";
public static final String REST = "reStructuredText";
public static final String NUMPY = "NumPy";
public static final String GOOGLE = "Google";
public static final List<String> ALL = ImmutableList.of(PLAIN, EPYTEXT, REST, NUMPY);
public static final List<String> ALL = ImmutableList.of(PLAIN, EPYTEXT, REST, NUMPY, GOOGLE);
private DocStringFormat() {
}
@@ -0,0 +1,106 @@
/*
* Copyright 2000-2015 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.documentation;
import com.intellij.openapi.util.Pair;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Mikhail Golubev
*/
public class GoogleCodeStyleDocString extends SectionBasedDocString {
private static final Pattern SECTION_HEADER_RE = Pattern.compile("^\\s*(.+?):\\s*$");
private static final Pattern FIELD_NAME_AND_TYPE_RE = Pattern.compile("\\s*(.+?)\\s*\\(\\s*(.+?)\\s*\\)\\s*");
public GoogleCodeStyleDocString(@NotNull String text) {
super(text);
}
@NotNull
@Override
protected Pair<SectionField, Integer> parseFieldWithType(int lineNum, int sectionIndent) {
return parseField(lineNum, sectionIndent, true);
}
@NotNull
@Override
protected Pair<SectionField, Integer> parseFieldWithNameAndOptionalType(int lineNum, int sectionIndent) {
return parseField(lineNum, sectionIndent, false);
}
/**
* <h3>Example</h3>
* <pre><code>
* Attributes:
* arg1 (int): field with name and optional type before description
*
* Raises:
* RuntimeException: field with only type before description
* </code></pre>
*
* @param typeBeforeColon according to Google Code Style there can be either type or name and type in parenthesis before colon
*/
@NotNull
private Pair<SectionField, Integer> parseField(int lineNum, int sectionIndent, boolean typeBeforeColon) {
Substring name = null, type = null, description;
final List<Substring> parts = getLine(lineNum).split(":", 1);
assert parts.size() <= 2;
if (parts.size() < 2) {
return Pair.create(null, lineNum);
}
final Substring textBeforeColon = parts.get(0);
if (typeBeforeColon) {
type = textBeforeColon.trim();
}
else {
// TODO skip references in types like Napoleon does
final Matcher matcher = FIELD_NAME_AND_TYPE_RE.matcher(textBeforeColon);
if (matcher.matches()) {
name = Substring.fromMatcherGroup(textBeforeColon, matcher, 1).trim();
type = Substring.fromMatcherGroup(textBeforeColon, matcher, 2).trim();
}
else {
name = textBeforeColon.trim();
}
}
description = parts.get(1);
final Pair<List<Substring>, Integer> pair = parseIndentedBlock(lineNum + 1, getLineIndent(lineNum), sectionIndent);
final List<Substring> nestedBlock = pair.getFirst();
if (!nestedBlock.isEmpty()) {
//noinspection ConstantConditions
description = description.getSmallestInclusiveSubstring(ContainerUtil.getLastItem(nestedBlock));
}
assert description != null;
description = description.trim();
return Pair.create(new SectionField(name, type, description), pair.getSecond());
}
@NotNull
@Override
protected Pair<String, Integer> parseSectionHeader(int lineNum) {
final Matcher matcher = SECTION_HEADER_RE.matcher(getLine(lineNum));
if (matcher.matches()) {
return Pair.create(matcher.group(1).trim(), lineNum + 1);
}
return Pair.create(null, lineNum);
}
}
@@ -0,0 +1,440 @@
/*
* Copyright 2000-2015 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.documentation;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.psi.StructuredDocString;
import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Common base class for docstring styles supported by Napoleon Sphinx extension.
*
* @author Mikhail Golubev
* @see <a href="http://sphinxcontrib-napoleon.readthedocs.org/en/latest/index.html">Napoleon</a>
*/
public abstract class SectionBasedDocString implements StructuredDocString {
private static final Map<String, String> SECTION_ALIASES =
ImmutableMap.<String, String>builder()
.put("arguments", "parameters")
.put("args", "parameters")
.put("parameters", "parameters")
.put("keyword args", "keyword arguments")
.put("keyword arguments", "keyword arguments")
.put("other parameters", "other parameters")
.put("attributes", "attributes")
.put("methods", "methods")
.put("note", "notes")
.put("notes", "notes")
.put("example", "examples")
.put("examples", "examples")
.put("return", "returns")
.put("returns", "returns")
.put("yield", "yields")
.put("yields", "yields")
.put("raises", "raises")
.put("references", "references")
.put("see also", "see also")
.put("warning", "warnings")
.put("warns", "warnings")
.put("warnings", "warnings")
.build();
private static ImmutableSet<String> SECTIONS_WITH_NAME_AND_TYPE = ImmutableSet.of("attributes", "methods",
"parameters", "keyword arguments", "other parameters");
private static ImmutableSet<String> SECTIONS_WITH_TYPE = ImmutableSet.of("returns", "raises", "yields");
protected final List<Substring> myLines;
private final List<Substring> myOtherContent = new ArrayList<Substring>();
private final List<Section> mySections = new ArrayList<Section>();
private final String mySummary;
protected SectionBasedDocString(@NotNull String text) {
myLines = new Substring(text).splitLines();
String summary = "";
int lineNum = 0;
while (lineNum < myLines.size()) {
Pair<Section, Integer> result = parseSection(lineNum);
if (result.getFirst() != null) {
mySections.add(result.getFirst());
lineNum = result.getSecond();
}
else {
if (lineNum == 0 && isEmptyOrDoesNotExist(lineNum + 1)) {
summary = getLine(0).trim().toString();
}
else {
myOtherContent.add(getLine(lineNum));
}
lineNum++;
}
}
mySummary = summary;
}
@NotNull
protected Pair<Section, Integer> parseSection(int sectionStartLine) {
final Pair<String, Integer> pair = parseSectionHeader(sectionStartLine);
final String title = normalizeSectionTitle(pair.getFirst());
if (title == null) {
return Pair.create(null, sectionStartLine);
}
int lineNum = skipEmptyLines(pair.getSecond());
final List<SectionField> fields = new ArrayList<SectionField>();
final int sectionIndent = getLineIndent(sectionStartLine);
while (!isSectionBreak(lineNum, sectionIndent)) {
final Pair<SectionField, Integer> result = parseField(lineNum, title, sectionIndent);
if (result.getFirst() == null) {
break;
}
fields.add(result.getFirst());
lineNum = skipEmptyLines(result.getSecond());
}
return Pair.create(new Section(title, fields), lineNum);
}
@NotNull
protected Pair<SectionField, Integer> parseField(int lineNum, @NotNull String sectionTitle, int sectionIndent) {
if (SECTIONS_WITH_NAME_AND_TYPE.contains(sectionTitle)) {
return parseFieldWithNameAndOptionalType(lineNum, sectionIndent);
}
if (SECTIONS_WITH_TYPE.contains(sectionTitle)) {
return parseFieldWithType(lineNum, sectionIndent);
}
return parseGeneralField(lineNum, sectionIndent);
}
@NotNull
protected Pair<SectionField,Integer> parseGeneralField(int lineNum, int sectionIndent) {
final Pair<List<Substring>, Integer> pair = parseIndentedBlock(lineNum, sectionIndent, sectionIndent);
final Substring firstLine = ContainerUtil.getFirstItem(pair.getFirst());
final Substring lastLine = ContainerUtil.getLastItem(pair.getFirst());
if (firstLine != null && lastLine != null) {
final Substring mergedSubstring = new Substring(firstLine.getSuperString(), firstLine.getStartOffset(), lastLine.getEndOffset());
return Pair.create(new SectionField(null, null, mergedSubstring), pair.getSecond());
}
return Pair.create(null, pair.getSecond());
}
@NotNull
protected abstract Pair<SectionField,Integer> parseFieldWithType(int lineNum, int sectionIndent);
@NotNull
protected abstract Pair<SectionField,Integer> parseFieldWithNameAndOptionalType(int lineNum, int sectionIndent);
@NotNull
protected abstract Pair<String, Integer> parseSectionHeader(int lineNum);
protected int getLineIndent(int lineNum) {
final Substring line = getLine(lineNum);
for (int i = 0; i < line.length(); i++) {
if (!Character.isSpaceChar(line.charAt(i))) {
return i;
}
}
return 0;
}
private int skipEmptyLines(int lineNum) {
while (lineNum < myLines.size() && isEmpty(lineNum)) {
lineNum++;
}
return lineNum;
}
@Nullable
protected String normalizeSectionTitle(@Nullable @NonNls String title) {
return title == null ? null : SECTION_ALIASES.get(title.toLowerCase());
}
private boolean isEmptyOrDoesNotExist(int lineNum) {
return lineNum >= myLines.size() - 1 || isEmpty(lineNum);
}
private boolean isEmpty(int lineNum) {
return StringUtil.isEmptyOrSpaces(getLine(lineNum));
}
private boolean isSectionStart(int lineNum) {
final Pair<String, Integer> pair = parseSectionHeader(lineNum);
return pair.getFirst() != null;
}
private boolean isSectionBreak(int lineNum, int curSectionIndent) {
return lineNum >= myLines.size() ||
isSectionStart(lineNum) ||
(!isEmpty(lineNum) && getLineIndent(lineNum) <= curSectionIndent);
}
@NotNull
protected Pair<List<Substring>, Integer> parseIndentedBlock(int lineNum, int blockIndent, int sectionIndent) {
final List<Substring> result = new ArrayList<Substring>();
while (!isSectionBreak(lineNum, sectionIndent) && (isEmpty(lineNum) || getLineIndent(lineNum) > blockIndent)) {
result.add(getLine(lineNum));
lineNum++;
}
return Pair.create(result, lineNum);
}
@NotNull
protected Substring getLine(int indent) {
return myLines.get(indent);
}
@VisibleForTesting
public List<Section> getSections() {
return Collections.unmodifiableList(mySections);
}
@NotNull
@Override
public String createParameterType(@NotNull String name, @NotNull String type) {
return null;
}
@Override
public String getSummary() {
return mySummary.toString();
}
@Override
public String getDescription() {
return null;
}
@Override
public List<String> getParameters() {
return null;
}
@Override
public List<Substring> getParameterSubstrings() {
return null;
}
@Nullable
@Override
public String getParamType(@Nullable String paramName) {
return null;
}
@Nullable
@Override
public Substring getParamTypeSubstring(@Nullable String paramName) {
return null;
}
@Nullable
@Override
public String getParamDescription(@Nullable String paramName) {
return null;
}
@Override
public List<String> getKeywordArguments() {
return null;
}
@Override
public List<Substring> getKeywordArgumentSubstrings() {
return null;
}
@Nullable
@Override
public String getKeywordArgumentDescription(@Nullable String paramName) {
return null;
}
@Nullable
@Override
public String getReturnType() {
return null;
}
@Nullable
@Override
public Substring getReturnTypeSubstring() {
return null;
}
@Nullable
@Override
public String getReturnDescription() {
return null;
}
@Override
public List<String> getRaisedExceptions() {
return null;
}
@Nullable
@Override
public String getRaisedExceptionDescription(@Nullable String exceptionName) {
return null;
}
@Nullable
@Override
public String getAttributeDescription() {
return null;
}
@Nullable
@Override
public Substring getTagValue(String... tagNames) {
return null;
}
@Nullable
@Override
public Substring getTagValue(String tagName, @NotNull String argName) {
return null;
}
@Nullable
@Override
public Substring getTagValue(String[] tagNames, @NotNull String argName) {
return null;
}
@Override
public List<Substring> getTagArguments(String... tagNames) {
return null;
}
@Nullable
@Override
public Substring getParamByNameAndKind(@NotNull String name, String kind) {
return null;
}
@Override
public List<String> getAdditionalTags() {
return null;
}
public static class Section {
private final String myTitle;
private final List<SectionField> myFields;
public Section(@NotNull String title, @NotNull List<SectionField> fields) {
myTitle = title;
myFields = new ArrayList<SectionField>(fields);
}
@NotNull
public String getTitle() {
return myTitle;
}
@NotNull
public List<SectionField> getFields() {
return Collections.unmodifiableList(myFields);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Section section = (Section)o;
if (!myTitle.equals(section.myTitle)) return false;
if (!myFields.equals(section.myFields)) return false;
return true;
}
@Override
public int hashCode() {
int result = myTitle.hashCode();
result = 31 * result + myFields.hashCode();
return result;
}
}
public static class SectionField {
private final Substring myName;
private final Substring myType;
private final Substring myDescription;
public SectionField(@Nullable Substring name, @Nullable Substring type, @Nullable Substring description) {
myName = name;
myType = type;
myDescription = description;
}
@Nullable
public Substring getName() {
return myName;
}
@Nullable
public Substring getType() {
return myType;
}
@Nullable
public Substring getDescription() {
return myDescription;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SectionField field = (SectionField)o;
if (myName != null ? !myName.equals(field.myName) : field.myName != null) return false;
if (myType != null ? !myType.equals(field.myType) : field.myType != null) return false;
if (myDescription != null ? !myDescription.equals(field.myDescription) : field.myDescription != null) return false;
return true;
}
@Override
public int hashCode() {
int result = myName != null ? myName.hashCode() : 0;
result = 31 * result + (myType != null ? myType.hashCode() : 0);
result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0);
return result;
}
}
// MethodsSection
// AttributesSection
// YieldsSection
// RaisesSection
// ReturnsSection
}
@@ -0,0 +1,14 @@
def func(x, y, *args, **kwargs):
"""Summary
Parameters:
x (int) : first parameter
y: second parameter
with longer description
Raises:
Exception: if anything bad happens
Returns:
None: always
"""
pass
@@ -0,0 +1,110 @@
/*
* Copyright 2000-2015 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;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.documentation.GoogleCodeStyleDocString;
import com.jetbrains.python.documentation.SectionBasedDocString.Section;
import com.jetbrains.python.documentation.SectionBasedDocString.SectionField;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.PyStringLiteralExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* @author Mikhail Golubev
*/
public class PyGoogleCodeStyleDocStringTest extends PyTestCase {
public void testSimpleFunctionDocString() {
myFixture.configureByFile(getTestName(true) + ".py");
final String docStringText = findFirstDocString();
assertNotNull(docStringText);
final GoogleCodeStyleDocString docString = new GoogleCodeStyleDocString(docStringText);
assertEquals("Summary", docString.getSummary());
final List<Section> sections = docString.getSections();
assertSize(3, sections);
assertEquals("parameters", sections.get(0).getTitle());
final List<SectionField> paramFields = sections.get(0).getFields();
assertSize(2, paramFields);
final SectionField firstParamField = paramFields.get(0);
assertNotNull(firstParamField.getName());
assertEquals("x", firstParamField.getName().toString());
assertNotNull(firstParamField.getType());
assertEquals("int", firstParamField.getType().toString());
assertNotNull(firstParamField.getDescription());
assertEquals("first parameter", firstParamField.getDescription().toString());
final SectionField secondParamField = paramFields.get(1);
assertNotNull(secondParamField.getName());
assertEquals("y", secondParamField.getName().toString());
assertNull(secondParamField.getType());
assertNotNull(secondParamField.getDescription());
assertEquals("second parameter\n" +
" with longer description", secondParamField.getDescription().toString());
assertEquals("raises", sections.get(1).getTitle());
final List<SectionField> exceptionFields = sections.get(1).getFields();
assertSize(1, exceptionFields);
final SectionField firstExcField = exceptionFields.get(0);
assertNull(firstExcField.getName());
assertNotNull(firstExcField.getType());
assertEquals("Exception", firstExcField.getType().toString());
assertNotNull(firstExcField.getDescription());
assertEquals("if anything bad happens", firstExcField.getDescription().toString());
assertEquals("returns", sections.get(2).getTitle());
final List<SectionField> returnFields = sections.get(2).getFields();
assertSize(1, returnFields);
final SectionField firstReturnField = returnFields.get(0);
assertNull(firstReturnField.getName());
assertNotNull(firstReturnField.getType());
assertEquals("None", firstReturnField.getType().toString());
assertNotNull(firstReturnField.getDescription());
assertEquals("always", firstReturnField.getDescription().toString());
}
@Nullable
private String findFirstDocString() {
final PsiElementProcessor.FindElement<PsiElement> processor = new PsiElementProcessor.FindElement<PsiElement>() {
@Override
public boolean execute(@NotNull PsiElement element) {
if (element instanceof PyStringLiteralExpression && element.getFirstChild().getNode().getElementType() == PyTokenTypes.DOCSTRING) {
return setFound(element);
}
return true;
}
};
PsiTreeUtil.processElements(myFixture.getFile(), processor);
if (!processor.isFound()) {
return null;
}
final PsiElement foundElement = processor.getFoundElement();
assertNotNull(foundElement);
return ((PyStringLiteralExpression)foundElement).getStringValue();
}
@Override
protected String getTestDataPath() {
return super.getTestDataPath() + "/docstrings";
}
}