PY-16908 Support combined parameter declarations in Numpy docstrings
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Thu, 17 Sep 2015 14:49:36 +0000 (17:49 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Thu, 17 Sep 2015 15:48:46 +0000 (18:48 +0300)
25 files changed:
python/src/com/jetbrains/python/documentation/docstrings/DocStringReferenceProvider.java
python/src/com/jetbrains/python/documentation/docstrings/NumpyDocString.java
python/src/com/jetbrains/python/documentation/docstrings/NumpyDocStringUpdater.java
python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocString.java
python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java
python/testData/docstrings/numpyCombinedParamDeclarations.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveCombinedVarargParam.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveCombinedVarargParam_after.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveFirstOfCombinedParams.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveFirstOfCombinedParams_after.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveLastOfCombinedParams.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveLastOfCombinedParams_after.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveMidOfCombinedParams.py [new file with mode: 0644]
python/testData/inspections/NumpyDocStringRemoveMidOfCombinedParams_after.py [new file with mode: 0644]
python/testData/intentions/afterParamTypeInNumpyDocStringCombinedParams.py [new file with mode: 0644]
python/testData/intentions/afterParamTypeInNumpyDocStringCombinedParamsColon.py [new file with mode: 0644]
python/testData/intentions/beforeParamTypeInNumpyDocStringCombinedParams.py [new file with mode: 0644]
python/testData/intentions/beforeParamTypeInNumpyDocStringCombinedParamsColon.py [new file with mode: 0644]
python/testData/refactoring/rename/numpyDocStringCombinedParam.py [new file with mode: 0644]
python/testData/refactoring/rename/numpyDocStringCombinedParam_after.py [new file with mode: 0644]
python/testSrc/com/jetbrains/python/Py3QuickFixTest.java
python/testSrc/com/jetbrains/python/PyQuickFixTest.java
python/testSrc/com/jetbrains/python/PySectionBasedDocStringTest.java
python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java
python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java

index d6cb3876da28a40707c2c679553cdc3eecd18a6c..9d83f9c5f33149dae12b45a09afd535ee89de1ad 100644 (file)
@@ -130,10 +130,11 @@ public class DocStringReferenceProvider extends PsiReferenceProvider {
                                                          @Nullable DocStringParameterReference.ReferenceType nameRefType) {
     final List<PsiReference> result = new ArrayList<PsiReference>();
     for (SectionBasedDocString.SectionField field : fields) {
-      final Substring nameSub = field.getNameAsSubstring();
-      if (nameRefType != null && nameSub != null && !nameSub.isEmpty()) {
-        final TextRange range = nameSub.getTextRange().shiftRight(offset);
-        result.add(new DocStringParameterReference(element, range, nameRefType));
+      for (Substring nameSub: field.getNamesAsSubstrings()) {
+        if (nameRefType != null && nameSub != null && !nameSub.isEmpty()) {
+          final TextRange range = nameSub.getTextRange().shiftRight(offset);
+          result.add(new DocStringParameterReference(element, range, nameRefType));
+        }
       }
       final Substring typeSub = field.getTypeAsSubstring();
       if (typeSub != null && !typeSub.isEmpty()) {
index 852d7f05537208d7c7373847d1de4b226577f2c1..12ee80b2f65b32a992c5d10c5d654d7760266312 100644 (file)
@@ -20,6 +20,7 @@ import com.jetbrains.python.toolbox.Substring;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Pattern;
 
@@ -30,6 +31,7 @@ import java.util.regex.Pattern;
  */
 public class NumpyDocString extends SectionBasedDocString {
   private static final Pattern SIGNATURE = Pattern.compile("^[ \t]*([\\w., ]+=)?[ \t]*[\\w\\.]+\\(.*\\)[ \t]*$", Pattern.MULTILINE);
+  private static final Pattern NAME_SEPARATOR = Pattern.compile("[ \t]*,[ \t]*");
   public static final Pattern SECTION_HEADER = Pattern.compile("^[ \t]*[-=]{2,}[ \t]*$", Pattern.MULTILINE);
 
   private Substring mySignature;
@@ -67,25 +69,35 @@ public class NumpyDocString extends SectionBasedDocString {
                                                           boolean mayHaveType,
                                                           boolean preferType) {
     final Substring line = getLine(lineNum);
-    Substring name, type = null, description = null;
+    Substring namesPart, type = null, description = null;
     if (mayHaveType) {
       final List<Substring> colonSeparatedParts = splitByFirstColon(line);
-      name = colonSeparatedParts.get(0).trim();
+      namesPart = colonSeparatedParts.get(0).trim();
       if (colonSeparatedParts.size() == 2) {
         type = colonSeparatedParts.get(1).trim();
       }
     }
     else {
-      name = line.trim();
+      namesPart = line.trim();
     }
     if (preferType && type == null) {
-      type = name;
-      name = null;
+      type = namesPart;
+      namesPart = null;
     }
-    if (name != null) {
-      name = cleanUpName(name);
+    final List<Substring> names = new ArrayList<Substring>();
+    if (namesPart != null) {
+      // Unlike Google code style, Numpydoc allows to list several parameter with same file together, e.g.
+      // x1, x2 : array_like
+      //     Input arrays, description of `x1`, `x2`.
+      for (Substring name : namesPart.split(NAME_SEPARATOR)) {
+        final Substring identifier = cleanUpName(name);
+        if (!isValidName(identifier.toString())) {
+          return Pair.create(null, lineNum);
+        }
+        names.add(identifier);
+      }
     }
-    if (name != null ? !isValidName(name.toString()) : !isValidType(type.toString())) {
+    if (namesPart == null && !isValidType(type.toString())) {
       return Pair.create(null, lineNum);
     }
     final Pair<List<Substring>, Integer> parsedDescription = parseIndentedBlock(lineNum + 1, getLineIndentSize(lineNum));
@@ -93,7 +105,7 @@ public class NumpyDocString extends SectionBasedDocString {
     if (!descriptionLines.isEmpty()) {
       description = descriptionLines.get(0).union(descriptionLines.get(descriptionLines.size() - 1));
     }
-    return Pair.create(new SectionField(name, type, description != null ? description.trim() : null), parsedDescription.getSecond());
+    return Pair.create(new SectionField(names, type, description != null ? description.trim() : null), parsedDescription.getSecond());
   }
 
   @NotNull
index a0a77cfd7c08db1b2bfdec05cd196f3538188991..b75d3f3f471c81fa98210412ab2aea3422ebbab9 100644 (file)
@@ -30,7 +30,7 @@ public class NumpyDocStringUpdater extends SectionBasedDocStringUpdater {
 
   @Override
   protected void updateParamDeclarationWithType(@NotNull Substring nameSubstring, @NotNull String type) {
-    insert(nameSubstring.getEndOffset(), " : " + type);
+    insert(myOriginalDocString.getLine(nameSubstring.getEndLine()).trimRight().getEndOffset(), " : " + type);
   }
 
   @Override
index 9ef308f74f87e8928ba70501f725a042265427de..d0ba1cccbe74f1139f47bb78e7e707745a80a9c1 100644 (file)
@@ -209,7 +209,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
     final Substring firstLine = ContainerUtil.getFirstItem(pair.getFirst());
     final Substring lastLine = ContainerUtil.getLastItem(pair.getFirst());
     if (firstLine != null && lastLine != null) {
-      return Pair.create(new SectionField(null, null, firstLine.union(lastLine).trim()), pair.getSecond());
+      return Pair.create(new SectionField((Substring)null, null, firstLine.union(lastLine).trim()), pair.getSecond());
     }
     return Pair.create(null, pair.getSecond());
   }
@@ -297,10 +297,10 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   @NotNull
   @Override
   public List<String> getParameters() {
-    return ContainerUtil.map(getParameterFields(), new Function<SectionField, String>() {
+    return ContainerUtil.map(getParameterSubstrings(), new Function<Substring, String>() {
       @Override
-      public String fun(SectionField field) {
-        return field.getName();
+      public String fun(Substring substring) {
+        return substring.toString();
       }
     });
   }
@@ -308,12 +308,11 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   @NotNull
   @Override
   public List<Substring> getParameterSubstrings() {
-    return ContainerUtil.mapNotNull(getParameterFields(), new Function<SectionField, Substring>() {
-      @Override
-      public Substring fun(SectionField field) {
-        return field.getNameAsSubstring();
-      }
-    });
+    final List<Substring> result = new ArrayList<Substring>();
+    for (SectionField field : getParameterFields()) {
+      ContainerUtil.addAllNotNull(result, field.getNamesAsSubstrings());
+    }
+    return result;
   }
 
   @Nullable
@@ -357,7 +356,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
     return ContainerUtil.find(getParameterFields(), new Condition<SectionField>() {
       @Override
       public boolean value(SectionField field) {
-        return name.equals(field.getName());
+        return field.getNames().contains(name);
       }
     });
   }
@@ -379,25 +378,23 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   @NotNull
   @Override
   public List<String> getKeywordArguments() {
-    return ContainerUtil.mapNotNull(getKeywordArgumentFields(), new Function<SectionField, String>() {
-      @Override
-      public String fun(SectionField field) {
-        return field.getName();
-      }
-    });
+    final List<String> result = new ArrayList<String>();
+    for (SectionField field : getKeywordArgumentFields()) {
+      result.addAll(field.getNames());
+    }
+    return result;
   }
 
   @NotNull
   @Override
   public List<Substring> getKeywordArgumentSubstrings() {
-    return ContainerUtil.mapNotNull(getKeywordArgumentFields(), new Function<SectionField, Substring>() {
-      @Override
-      public Substring fun(SectionField field) {
-        return field.getNameAsSubstring();
-      }
-    });
+    final List<Substring> result = new ArrayList<Substring>();
+    for (SectionField field : getKeywordArgumentFields()) {
+      ContainerUtil.addAllNotNull(field.getNamesAsSubstrings());
+    }
+    return result;
   }
-
+  
   @Nullable
   @Override
   public String getKeywordArgumentDescription(@Nullable String paramName) {
@@ -424,7 +421,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
     return ContainerUtil.find(getKeywordArgumentFields(), new Condition<SectionField>() {
       @Override
       public boolean value(SectionField field) {
-        return name.equals(field.getName());
+        return field.getNames().contains(name);
       }
     });
   }
@@ -598,24 +595,43 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   }
 
   public static class SectionField {
-    private final Substring myName;
+    private final List<Substring> myNames;
     private final Substring myType;
     private final Substring myDescription;
 
     public SectionField(@Nullable Substring name, @Nullable Substring type, @Nullable Substring description) {
-      myName = name;
+      this(name == null ? Collections.<Substring>emptyList() : Collections.singletonList(name), type, description);
+    }
+
+    public SectionField(@NotNull List<Substring> names, @Nullable Substring type, @Nullable Substring description) {
+      myNames = names;
       myType = type;
       myDescription = description;
     }
 
     @NotNull
     public String getName() {
-      return myName == null ? "" : myName.toString();
+      return myNames.isEmpty() ? "" : myNames.get(0).toString();
     }
 
     @Nullable
     public Substring getNameAsSubstring() {
-      return myName;
+      return myNames.isEmpty() ? null : myNames.get(0);
+    }
+
+    @NotNull
+    public List<Substring> getNamesAsSubstrings() {
+      return myNames;
+    }
+
+    @NotNull
+    public List<String> getNames() {
+      return ContainerUtil.map(myNames, new Function<Substring, String>() {
+        @Override
+        public String fun(Substring substring) {
+          return substring.toString();
+        }
+      });
     }
 
     @NotNull
@@ -645,7 +661,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
 
       SectionField field = (SectionField)o;
 
-      if (myName != null ? !myName.equals(field.myName) : field.myName != null) return false;
+      if (myNames != null ? !myNames.equals(field.myNames) : field.myNames != 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;
 
@@ -654,7 +670,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
 
     @Override
     public int hashCode() {
-      int result = myName != null ? myName.hashCode() : 0;
+      int result = myNames != null ? myNames.hashCode() : 0;
       result = 31 * result + (myType != null ? myType.hashCode() : 0);
       result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0);
       return result;
index 83f4d8ead46cba704903e0c615310cc33c17b601..db7a7a92d6607c22eda324a54fd885997500dd1e 100644 (file)
@@ -91,30 +91,82 @@ public abstract class SectionBasedDocStringUpdater extends DocStringUpdater<Sect
   }
 
   @Override
-  public void removeParameter(@NotNull String name) {
+  public void removeParameter(@NotNull final String name) {
     for (Section section : myOriginalDocString.getParameterSections()) {
       final List<SectionField> sectionFields = section.getFields();
-      for (SectionField param : sectionFields) {
-        if (param.getName().equals(name)) {
-          final int endLine = getFieldEndLine(param);
-          if (sectionFields.size() == 1) {
-            removeLinesAndSpacesAfter(getSectionStartLine(section), endLine + 1);
+      for (SectionField field : sectionFields) {
+        final Substring nameSub = ContainerUtil.find(field.getNamesAsSubstrings(), new Condition<Substring>() {
+          @Override
+          public boolean value(Substring substring) {
+            return substring.toString().equals(name);
           }
-          else {
-            final int startLine = getFieldStartLine(param);
-            if (ContainerUtil.getLastItem(sectionFields) == param) {
-              removeLines(startLine, endLine + 1);
+        });
+        if (nameSub != null) {
+          if (field.getNamesAsSubstrings().size() == 1) {
+            final int endLine = getFieldEndLine(field);
+            if (sectionFields.size() == 1) {
+              removeLinesAndSpacesAfter(getSectionStartLine(section), endLine + 1);
             }
             else {
-              removeLinesAndSpacesAfter(startLine, endLine + 1);
+              final int startLine = getFieldStartLine(field);
+              if (ContainerUtil.getLastItem(sectionFields) == field) {
+                removeLines(startLine, endLine + 1);
+              }
+              else {
+                removeLinesAndSpacesAfter(startLine, endLine + 1);
+              }
             }
           }
+          else {
+            final Substring wholeParamName = expandParamNameSubstring(nameSub);
+            remove(wholeParamName.getStartOffset(), wholeParamName.getEndOffset());
+          }
           break;
         }
       }
     }
   }
 
+  @NotNull
+  private static Substring expandParamNameSubstring(@NotNull Substring name) {
+    final String superString = name.getSuperString();
+    int startWithStars = name.getStartOffset();
+    int prevNonWhitespace;
+    do {
+      prevNonWhitespace = skipWhitespaces(superString, startWithStars - 1, true);
+      if (prevNonWhitespace >= 0 && superString.charAt(prevNonWhitespace) == '*') {
+        startWithStars = prevNonWhitespace;
+      }
+      else {
+        break;
+      }
+    }
+    while (startWithStars >= 0);
+    if (prevNonWhitespace >= 0 && superString.charAt(prevNonWhitespace) == ',') {
+      return new Substring(superString, prevNonWhitespace, name.getEndOffset());
+    }
+    // end offset is always exclusive
+    final int nextNonWhitespace = skipWhitespaces(superString, name.getEndOffset(), false);
+    if (nextNonWhitespace < superString.length() && superString.charAt(nextNonWhitespace) == ',') {
+      // if we remove parameter with trailing comma (i.e. first parameter) remove whitespaces after it as well
+      return new Substring(superString, startWithStars, skipWhitespaces(superString, nextNonWhitespace + 1, false)); 
+    }
+    return name;
+  }
+
+  private static int skipWhitespaces(@NotNull String s, int start, boolean backward) {
+    int result = start;
+    while (start >= 0 && start < s.length() && " \t".indexOf(s.charAt(result)) >= 0) {
+      if (backward) {
+        result--;
+      }
+      else{
+        result++;
+      }
+    }
+    return result;
+  }
+
   @Override
   protected void beforeApplyingModifications() {
     final List<AddParameter> newParams = new ArrayList<AddParameter>();
diff --git a/python/testData/docstrings/numpyCombinedParamDeclarations.py b/python/testData/docstrings/numpyCombinedParamDeclarations.py
new file mode 100644 (file)
index 0000000..751c81f
--- /dev/null
@@ -0,0 +1,8 @@
+def f(x, *args, y, **kwargs):
+    """
+
+    Parameters
+    ----------
+    x, *args, **kwargs, y: Any
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveCombinedVarargParam.py b/python/testData/inspections/NumpyDocStringRemoveCombinedVarargParam.py
new file mode 100644 (file)
index 0000000..03bd50d
--- /dev/null
@@ -0,0 +1,7 @@
+def f():
+    """
+    Parameters
+    ==========
+    x, *ar<caret>gs, **kwargs
+        no one writes like that
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveCombinedVarargParam_after.py b/python/testData/inspections/NumpyDocStringRemoveCombinedVarargParam_after.py
new file mode 100644 (file)
index 0000000..5da353f
--- /dev/null
@@ -0,0 +1,7 @@
+def f():
+    """
+    Parameters
+    ==========
+    x, **kwargs
+        no one writes like that
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveFirstOfCombinedParams.py b/python/testData/inspections/NumpyDocStringRemoveFirstOfCombinedParams.py
new file mode 100644 (file)
index 0000000..ae9cba8
--- /dev/null
@@ -0,0 +1,8 @@
+def f():
+    """
+    Parameters
+    ==========
+
+    <caret>x, y, z : type
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveFirstOfCombinedParams_after.py b/python/testData/inspections/NumpyDocStringRemoveFirstOfCombinedParams_after.py
new file mode 100644 (file)
index 0000000..2195b2e
--- /dev/null
@@ -0,0 +1,8 @@
+def f():
+    """
+    Parameters
+    ==========
+
+    y, z : type
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveLastOfCombinedParams.py b/python/testData/inspections/NumpyDocStringRemoveLastOfCombinedParams.py
new file mode 100644 (file)
index 0000000..cf03812
--- /dev/null
@@ -0,0 +1,8 @@
+def f():
+    """
+    Parameters
+    ==========
+
+    x, y, <caret>z : type
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveLastOfCombinedParams_after.py b/python/testData/inspections/NumpyDocStringRemoveLastOfCombinedParams_after.py
new file mode 100644 (file)
index 0000000..b353077
--- /dev/null
@@ -0,0 +1,8 @@
+def f():
+    """
+    Parameters
+    ==========
+
+    x, y : type
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveMidOfCombinedParams.py b/python/testData/inspections/NumpyDocStringRemoveMidOfCombinedParams.py
new file mode 100644 (file)
index 0000000..0f1a30b
--- /dev/null
@@ -0,0 +1,8 @@
+def f():
+    """
+    Parameters
+    ==========
+
+    x, <caret>y, z : type
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/inspections/NumpyDocStringRemoveMidOfCombinedParams_after.py b/python/testData/inspections/NumpyDocStringRemoveMidOfCombinedParams_after.py
new file mode 100644 (file)
index 0000000..77b6d6c
--- /dev/null
@@ -0,0 +1,8 @@
+def f():
+    """
+    Parameters
+    ==========
+
+    x, z : type
+        description
+    """
\ No newline at end of file
diff --git a/python/testData/intentions/afterParamTypeInNumpyDocStringCombinedParams.py b/python/testData/intentions/afterParamTypeInNumpyDocStringCombinedParams.py
new file mode 100644 (file)
index 0000000..ce94aaa
--- /dev/null
@@ -0,0 +1,8 @@
+def f(x, y, z):
+    """
+    Parameters
+    ----------
+    
+    x, y, z : object
+        Description
+    """
\ No newline at end of file
diff --git a/python/testData/intentions/afterParamTypeInNumpyDocStringCombinedParamsColon.py b/python/testData/intentions/afterParamTypeInNumpyDocStringCombinedParamsColon.py
new file mode 100644 (file)
index 0000000..ce94aaa
--- /dev/null
@@ -0,0 +1,8 @@
+def f(x, y, z):
+    """
+    Parameters
+    ----------
+    
+    x, y, z : object
+        Description
+    """
\ No newline at end of file
diff --git a/python/testData/intentions/beforeParamTypeInNumpyDocStringCombinedParams.py b/python/testData/intentions/beforeParamTypeInNumpyDocStringCombinedParams.py
new file mode 100644 (file)
index 0000000..681b40f
--- /dev/null
@@ -0,0 +1,8 @@
+def f(x, <caret>y, z):
+    """
+    Parameters
+    ----------
+    
+    x, y, z
+        Description
+    """
\ No newline at end of file
diff --git a/python/testData/intentions/beforeParamTypeInNumpyDocStringCombinedParamsColon.py b/python/testData/intentions/beforeParamTypeInNumpyDocStringCombinedParamsColon.py
new file mode 100644 (file)
index 0000000..78c4e78
--- /dev/null
@@ -0,0 +1,8 @@
+def f(x, <caret>y, z):
+    """
+    Parameters
+    ----------
+    
+    x, y, z : 
+        Description
+    """
\ No newline at end of file
diff --git a/python/testData/refactoring/rename/numpyDocStringCombinedParam.py b/python/testData/refactoring/rename/numpyDocStringCombinedParam.py
new file mode 100644 (file)
index 0000000..049b956
--- /dev/null
@@ -0,0 +1,6 @@
+def f(foo, b<caret>az, quux):
+    """
+    Parameters
+    ----------
+    foo, baz, quux : str
+    """
\ No newline at end of file
diff --git a/python/testData/refactoring/rename/numpyDocStringCombinedParam_after.py b/python/testData/refactoring/rename/numpyDocStringCombinedParam_after.py
new file mode 100644 (file)
index 0000000..def9eb2
--- /dev/null
@@ -0,0 +1,6 @@
+def f(foo, bar, quux):
+    """
+    Parameters
+    ----------
+    foo, bar, quux : str
+    """
\ No newline at end of file
index 79af16fffce53a1c2b8f55900e05c7d10878196f..c00d677c85367e843eef4908f4a5c0b8c535d1ad 100644 (file)
@@ -217,6 +217,7 @@ public class Py3QuickFixTest extends PyTestCase {
    * @param available       true if the fix should be available, false if it should be explicitly not available.
    * @throws Exception
    */
+  @SuppressWarnings("Duplicates")
   protected void doInspectionTest(@NonNls @NotNull String[] testFiles,
                                   @NotNull Class inspectionClass,
                                   @NonNls @NotNull String quickFixName,
@@ -227,10 +228,14 @@ public class Py3QuickFixTest extends PyTestCase {
     myFixture.checkHighlighting(true, false, false);
     final List<IntentionAction> intentionActions = myFixture.filterAvailableIntentions(quickFixName);
     if (available) {
-      assertOneElement(intentionActions);
+      if (intentionActions.isEmpty()) {
+        throw new AssertionError("Quickfix \"" + quickFixName + "\" is not available");
+      }
+      if (intentionActions.size() > 1) {
+        throw new AssertionError("There are more than one quickfix with the name \"" + quickFixName + "\"");
+      }
       if (applyFix) {
         myFixture.launchAction(intentionActions.get(0));
-
         myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"));
       }
     }
index 1deb90622663108e0716c419b6b0196f59c52004..e1e9d76cdc3adf3579b0fa665fbe2eddc2017ee5 100644 (file)
@@ -534,6 +534,46 @@ public class PyQuickFixTest extends PyTestCase {
     });
   }
 
+  // PY-16908
+  public void testNumpyDocStringRemoveFirstOfCombinedParams() {
+    runWithDocStringFormat(DocStringFormat.NUMPY, new Runnable() {
+      @Override
+      public void run() {
+        doInspectionTest(PyDocstringInspection.class, PyBundle.message("QFIX.docstring.remove.$0", "x"), true, true);
+      }
+    });
+  }
+
+  // PY-16908
+  public void testNumpyDocStringRemoveMidOfCombinedParams() {
+    runWithDocStringFormat(DocStringFormat.NUMPY, new Runnable() {
+      @Override
+      public void run() {
+        doInspectionTest(PyDocstringInspection.class, PyBundle.message("QFIX.docstring.remove.$0", "y"), true, true);
+      }
+    });
+  }
+  
+  // PY-16908
+  public void testNumpyDocStringRemoveLastOfCombinedParams() {
+    runWithDocStringFormat(DocStringFormat.NUMPY, new Runnable() {
+      @Override
+      public void run() {
+        doInspectionTest(PyDocstringInspection.class, PyBundle.message("QFIX.docstring.remove.$0", "z"), true, true);
+      }
+    });
+  }
+
+  // PY-16908
+  public void testNumpyDocStringRemoveCombinedVarargParam() {
+    runWithDocStringFormat(DocStringFormat.NUMPY, new Runnable() {
+      @Override
+      public void run() {
+        doInspectionTest(PyDocstringInspection.class, PyBundle.message("QFIX.docstring.remove.$0", "args"), true, true);
+      }
+    });
+  }
+
   public void testUnnecessaryBackslash() {
     String[] testFiles = new String[]{"UnnecessaryBackslash.py"};
     myFixture.enableInspections(PyUnnecessaryBackslashInspection.class);
@@ -625,6 +665,7 @@ public class PyQuickFixTest extends PyTestCase {
    * @param available       true if the fix should be available, false if it should be explicitly not available.
    * @throws Exception
    */
+  @SuppressWarnings("Duplicates")
   protected void doInspectionTest(@NonNls @NotNull String[] testFiles,
                                   @NotNull Class inspectionClass,
                                   @NonNls @NotNull String quickFixName,
@@ -635,10 +676,14 @@ public class PyQuickFixTest extends PyTestCase {
     myFixture.checkHighlighting(true, false, false);
     final List<IntentionAction> intentionActions = myFixture.filterAvailableIntentions(quickFixName);
     if (available) {
-      assertOneElement(intentionActions);
+      if (intentionActions.isEmpty()) {
+        throw new AssertionError("Quickfix \"" + quickFixName + "\" is not available");
+      }
+      if (intentionActions.size() > 1) {
+        throw new AssertionError("There are more than one quickfix with the name \"" + quickFixName + "\"");
+      }
       if (applyFix) {
         myFixture.launchAction(intentionActions.get(0));
-
         myFixture.checkResultByFile(graftBeforeExt(testFiles[0], "_after"));
       }
     }
index 5d98447bb01c0f3550786826cd4534df52ddb8ad..97df4fca34d26c449f853bab6b40785e891d86f2 100644 (file)
@@ -351,6 +351,18 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     assertSize(2, returnSection.getFields());
   }
 
+  // PY-16908
+  public void testNumpyCombinedParamDeclarations() {
+    final NumpyDocString docString = findAndParseNumpyStyleDocString();
+    assertSize(1, docString.getSections());
+    final Section paramSection = docString.getSections().get(0);
+    assertSize(1, paramSection.getFields());
+    final SectionField firstField = paramSection.getFields().get(0);
+    assertSameElements(firstField.getNames(), "x", "y", "args", "kwargs");
+    assertEquals(firstField.getType(), "Any");
+    assertEquals(firstField.getDescription(), "description");
+  }
+
   @Override
   protected String getTestDataPath() {
     return super.getTestDataPath() + "/docstrings";
index e7ee154de3873b0bb588f455191df6965c26ebfb..976b98f8e2ed0f7c04a56e574de2b7a2a535f0f5 100644 (file)
@@ -619,7 +619,17 @@ public class  PyIntentionTest extends PyTestCase {
   public void testParamTypeInNumpyDocStringOtherSectionExists() {
     doDocParamTypeTest(DocStringFormat.NUMPY);
   }
-  
+
+  // PY-16908
+  public void testParamTypeInNumpyDocStringCombinedParams() {
+    doDocParamTypeTest(DocStringFormat.NUMPY);
+  }
+
+  // PY-16908
+  public void testParamTypeInNumpyDocStringCombinedParamsColon() {
+    doDocParamTypeTest(DocStringFormat.NUMPY);
+  }
+
   // PY-4717
   public void testReturnTypeInEmptyNumpyDocString() {
     doDocReturnTypeTest(DocStringFormat.NUMPY);
index 83655d41d80850448cf2a0de02d32012694b345f..8cadfe47a300fa2e573ecc301cf54952fbfbe00f 100644 (file)
@@ -203,36 +203,41 @@ public class PyRenameTest extends PyTestCase {
 
   // PY-9795
   public void testGoogleDocStringParam() {
-    renameWithDocStringFormat("bar");
+    renameWithDocStringFormat(DocStringFormat.GOOGLE, "bar");
   }
 
   // PY-9795
   public void testGoogleDocStringAttribute() {
-    renameWithDocStringFormat("bar");
+    renameWithDocStringFormat(DocStringFormat.GOOGLE, "bar");
   }
 
   // PY-9795
   public void testGoogleDocStringParamType() {
-    renameWithDocStringFormat("Bar");
+    renameWithDocStringFormat(DocStringFormat.GOOGLE, "Bar");
   }
 
   // PY-9795
   public void testGoogleDocStringReturnType() {
-    renameWithDocStringFormat("Bar");
+    renameWithDocStringFormat(DocStringFormat.GOOGLE, "Bar");
   }
 
   // PY-16761
   public void testGoogleDocStringPositionalVararg() {
-    renameWithDocStringFormat("bar");
+    renameWithDocStringFormat(DocStringFormat.GOOGLE, "bar");
   }
 
   // PY-16761
   public void testGoogleDocStringKeywordVararg() {
-    renameWithDocStringFormat("bar");
+    renameWithDocStringFormat(DocStringFormat.GOOGLE, "bar");
   }
 
-  private void renameWithDocStringFormat(final String newName) {
-    runWithDocStringFormat(DocStringFormat.GOOGLE, new Runnable() {
+  // PY-16908
+  public void testNumpyDocStringCombinedParam() {
+    renameWithDocStringFormat(DocStringFormat.NUMPY, "bar");
+  }
+
+  private void renameWithDocStringFormat(DocStringFormat format, final String newName) {
+    runWithDocStringFormat(format, new Runnable() {
       public void run() {
         doTest(newName);
       }