Skip to content

Commit 1bf88fa

Browse files
avokinintellij-monorepo-bot
authored andcommitted
PY-88579 make PyTypingTypeProvider order higher than docstring type providers
PY-32793 favor type annotations over types in Numpy docstrings GitOrigin-RevId: 17dd200a1f623941781ed583c712abcb9d2baedc
1 parent fc34e06 commit 1bf88fa

6 files changed

Lines changed: 260 additions & 5 deletions

File tree

python/python-psi-impl/resources/intellij.python.psi.impl.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@
692692

693693
<!-- NumPy -->
694694
<typeProvider implementation="com.jetbrains.python.numpy.codeInsight.NumpyDocStringTypeProvider"
695-
order="before pythonDocstringTypeProvider"/>
695+
order="before pythonDocstringTypeProvider, after pyTypingTypeProvider"/>
696696
<pyClassMembersProvider implementation="com.jetbrains.python.numpy.codeInsight.NumpyClassMembersProvider"/>
697697
<resolveResultRater implementation="com.jetbrains.python.numpy.codeInsight.NumpyResolveRater"/>
698698
<pyiStubSuppressor implementation="com.jetbrains.python.numpy.codeInsight.NumpyPyiStubsSuppressor"/>

python/python-psi-impl/src/com/jetbrains/python/numpy/codeInsight/NumpyDocStringTypeProvider.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,49 @@ public final class NumpyDocStringTypeProvider extends PyTypeProviderBase {
199199
if (index >= 0) {
200200
return typeString.substring(0, index);
201201
}
202+
203+
// Match ", default None" with various separators and spacings:
204+
// ", default None", ", default=None", ", default:None",
205+
// ", default: None", ", default = None", etc.
206+
int defaultIndex = findDefaultSuffixStart(typeString);
207+
if (defaultIndex >= 0 && isDefaultValueNone(typeString, defaultIndex)) {
208+
return typeString.substring(0, defaultIndex);
209+
}
210+
202211
return null;
203212
}
204213

214+
/**
215+
* Strips ", default <value>" from the type string regardless of the default value.
216+
* Returns the original string when no such suffix is present.
217+
* Unlike {@link #cleanupOptional}, this does not imply the parameter is Optional.
218+
*/
219+
public static @NotNull String stripDefaultValue(@NotNull String typeString) {
220+
int index = findDefaultSuffixStart(typeString);
221+
return index >= 0 ? typeString.substring(0, index) : typeString;
222+
}
223+
224+
private static int findDefaultSuffixStart(@NotNull String typeString) {
225+
int index = typeString.indexOf(", default");
226+
if (index < 0) return -1;
227+
int after = index + ", default".length();
228+
if (after >= typeString.length()) return -1;
229+
char nextChar = typeString.charAt(after);
230+
return (nextChar == ' ' || nextChar == '=' || nextChar == ':') ? index : -1;
231+
}
232+
233+
private static boolean isDefaultValueNone(@NotNull String typeString, int defaultStart) {
234+
int pos = defaultStart + ", default".length();
235+
while (pos < typeString.length() && Character.isWhitespace(typeString.charAt(pos))) pos++;
236+
if (pos < typeString.length() && (typeString.charAt(pos) == '=' || typeString.charAt(pos) == ':')) {
237+
pos++;
238+
while (pos < typeString.length() && Character.isWhitespace(typeString.charAt(pos))) pos++;
239+
}
240+
if (!typeString.startsWith("None", pos)) return false;
241+
if (pos + 4 == typeString.length()) return true;
242+
return !Character.isJavaIdentifierPart(typeString.charAt(pos + 4));
243+
}
244+
205245
public static @NotNull List<String> getNumpyUnionType(@NotNull String typeString) {
206246
final Matcher arrayMatcher = NUMPY_ARRAY_PATTERN.matcher(typeString);
207247
if (arrayMatcher.matches()) {
@@ -371,6 +411,9 @@ private static PyPsiFacade getPsiFacade(@NotNull PsiElement anchor) {
371411
if (withoutOptional != null) {
372412
typeString = withoutOptional;
373413
}
414+
else {
415+
typeString = stripDefaultValue(typeString);
416+
}
374417
for (String typeName : getNumpyUnionType(typeString)) {
375418
PyType parsedType = parseSingleNumpyDocType(anchor, typeName);
376419
if (parsedType != null) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
def bar(self, baz):
2+
"""
3+
Text
4+
5+
Parameters
6+
----------
7+
baz : str, default None
8+
A parameter with default None. Should not trigger type error.
9+
"""
10+
pass
11+
12+
13+
def bar_colon(self, baz):
14+
"""
15+
Parameters
16+
----------
17+
baz : str, default:None
18+
"""
19+
pass
20+
21+
22+
def bar_colon_space(self, baz):
23+
"""
24+
Parameters
25+
----------
26+
baz : str, default: None
27+
"""
28+
pass
29+
30+
31+
def bar_equals(self, baz):
32+
"""
33+
Parameters
34+
----------
35+
baz : str, default=None
36+
"""
37+
pass
38+
39+
40+
def bar_equals_spaces(self, baz):
41+
"""
42+
Parameters
43+
----------
44+
baz : str, default = None
45+
"""
46+
pass
47+
48+
49+
bar(None, None)
50+
bar(None, "test")
51+
bar(None, baz=None)
52+
bar(None, baz="test")
53+
54+
bar_colon(None, None)
55+
bar_colon(None, baz=None)
56+
bar_colon(None, baz="test")
57+
58+
bar_colon_space(None, None)
59+
bar_colon_space(None, baz=None)
60+
bar_colon_space(None, baz="test")
61+
62+
bar_equals(None, None)
63+
bar_equals(None, baz=None)
64+
bar_equals(None, baz="test")
65+
66+
bar_equals_spaces(None, None)
67+
bar_equals_spaces(None, baz=None)
68+
bar_equals_spaces(None, baz="test")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
def test_default_true(copy):
2+
"""
3+
Text
4+
5+
Parameters
6+
----------
7+
copy : bool, default True
8+
Whether to copy data
9+
"""
10+
pass
11+
12+
13+
def test_default_equals_true(copy):
14+
"""
15+
Text
16+
17+
Parameters
18+
----------
19+
copy : bool, default=True
20+
Whether to copy data
21+
"""
22+
pass
23+
24+
25+
def test_default_colon_true(copy):
26+
"""
27+
Text
28+
29+
Parameters
30+
----------
31+
copy : bool, default: True
32+
Whether to copy data
33+
"""
34+
pass
35+
36+
37+
def test_default_false(inplace):
38+
"""
39+
Text
40+
41+
Parameters
42+
----------
43+
inplace : bool, default False
44+
Whether to modify in place
45+
"""
46+
pass
47+
48+
49+
def test_default_integer(count):
50+
"""
51+
Text
52+
53+
Parameters
54+
----------
55+
count : int, default 10
56+
Number of items
57+
"""
58+
pass
59+
60+
61+
def test_default_string(name):
62+
"""
63+
Text
64+
65+
Parameters
66+
----------
67+
name : str, default "test"
68+
Name parameter
69+
"""
70+
pass
71+
72+
73+
# Matching the documented type should not trigger warnings
74+
test_default_true(copy=True)
75+
test_default_true(copy=False)
76+
77+
test_default_equals_true(copy=True)
78+
79+
test_default_colon_true(copy=False)
80+
81+
test_default_false(inplace=True)
82+
83+
test_default_integer(count=5)
84+
85+
test_default_string(name="custom")
86+
87+
# A non-None default must not make the parameter Optional
88+
test_default_true(<warning descr="Expected type 'bool', got 'None' instead">copy=None</warning>)
89+
test_default_equals_true(<warning descr="Expected type 'bool', got 'None' instead">copy=None</warning>)
90+
test_default_colon_true(<warning descr="Expected type 'bool', got 'None' instead">copy=None</warning>)
91+
test_default_string(<warning descr="Expected type 'str', got 'None' instead">name=None</warning>)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
def process_data_with_type_hint(name: int):
2+
"""
3+
Text
4+
5+
Parameters
6+
----------
7+
name : str
8+
Name parameter
9+
"""
10+
pass
11+
12+
13+
def process_data_without_type_hint(name):
14+
"""
15+
Text
16+
17+
Parameters
18+
----------
19+
name : str
20+
Name parameter
21+
"""
22+
pass
23+
24+
process_data_with_type_hint(name=10)
25+
process_data_without_type_hint(<warning descr="Expected type 'str', got 'int' instead">name=10</warning>)

python/testSrc/com/jetbrains/python/inspections/PyNumpyTypeTest.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.jetbrains.python.inspections;
1717

18+
import com.jetbrains.python.documentation.PyDocumentationSettings;
19+
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
1820
import com.jetbrains.python.fixtures.PyTestCase;
1921

2022
public class PyNumpyTypeTest extends PyTestCase {
@@ -27,9 +29,23 @@ protected void setUp() throws Exception {
2729
}
2830

2931
private void doTest() {
30-
myFixture.configureByFile(TEST_DIRECTORY + getTestName(false) + ".py");
31-
myFixture.enableInspections(PyTypeCheckerInspection.class);
32-
myFixture.checkHighlighting(true, false, true);
32+
doTest(true);
33+
}
34+
35+
private void doTest(boolean enableNumpyDocStringFormat) {
36+
PyDocumentationSettings settings = PyDocumentationSettings.getInstance(myFixture.getModule());
37+
DocStringFormat originalFormat = settings.getFormat();
38+
try {
39+
if (enableNumpyDocStringFormat) {
40+
settings.setFormat(DocStringFormat.NUMPY);
41+
}
42+
myFixture.configureByFile(TEST_DIRECTORY + getTestName(false) + ".py");
43+
myFixture.enableInspections(PyTypeCheckerInspection.class);
44+
myFixture.checkHighlighting(true, false, true);
45+
}
46+
finally {
47+
settings.setFormat(originalFormat);
48+
}
3349
}
3450

3551
public void testNominalType() {
@@ -64,6 +80,14 @@ public void testDefaultValueKeyword() {
6480
doTest();
6581
}
6682

83+
public void testDefaultNone() {
84+
doTest();
85+
}
86+
87+
public void testDefaultValueVariants() {
88+
doTest();
89+
}
90+
6791
public void testSort() {
6892
doTest();
6993
}
@@ -72,10 +96,14 @@ public void testArrayLike() {
7296
doTest();
7397
}
7498

75-
public void testUFunc() {
99+
public void testTypeHintHasPriority() {
76100
doTest();
77101
}
78102

103+
public void testUFunc() {
104+
doTest(false);
105+
}
106+
79107
public void testUFuncMixedWithNumber() {
80108
doTest();
81109
}

0 commit comments

Comments
 (0)