Skip to content

Commit 2f60655

Browse files
committed
Provide all typing hints if typing is not imported
1 parent 2828100 commit 2f60655

File tree

2 files changed

+105
-43
lines changed

2 files changed

+105
-43
lines changed

src/typingHintProvider.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { VariableSearchResult, TypeSearch } from "./typeSearch";
88
export class TypingHintProvider {
99

1010
private typeContainer: DataTypeContainer;
11+
private typingImportDetected: boolean = false;
1112
private fromTypingImport: boolean = false;
1213
private providedTypes: DataType[] = [];
1314
private typingPrefix: string = "typing";
@@ -28,27 +29,20 @@ export class TypingHintProvider {
2829
* @returns True if typing is imported.
2930
*/
3031
public async detectTypingImport(docText: string): Promise<boolean> {
31-
let m = new RegExp(
32-
`^[ \t]*from typing import +([A-Z][a-zA-Z0-9 ,]+)`,
33-
"m"
34-
).exec(docText);
35-
36-
if (m) {
32+
33+
if (/^[ \t]*from typing import +([A-Z][a-zA-Z0-9 ,]+)/m.exec(docText)) {
3734
this.fromTypingImport = true;
38-
return true;
35+
this.typingImportDetected = true;
3936
} else {
40-
m = new RegExp(
41-
`^[ \t]*(import +typing +as +([a-zA-Z_][a-zA-Z0-9_-]*)|import +typing)`,
42-
"m"
43-
).exec(docText);
44-
if (m) {
45-
if (m[2]) {
46-
this.typingPrefix = m[2];
37+
const match = /^[ \t]*(import +typing +as +([a-zA-Z_][a-zA-Z0-9_-]*)|import +typing)/m.exec(docText);
38+
if (match) {
39+
if (match[2]) {
40+
this.typingPrefix = match[2];
4741
}
48-
return true;
42+
this.typingImportDetected = true;
4943
}
5044
}
51-
return false;
45+
return this.typingImportDetected;
5246
}
5347

5448
/**
@@ -61,7 +55,7 @@ export class TypingHintProvider {
6155
const type = this.typeContainer[typeName];
6256
if (type.category === TypeCategory.Collection) {
6357
this.providedTypes.push(type);
64-
return this.toTypingString(type.name);
58+
return this.toTypingString(type.name, this.fromTypingImport);
6559
}
6660
return null;
6761
}
@@ -78,7 +72,7 @@ export class TypingHintProvider {
7872
const type = this.typeContainer[searchResult.typeName];
7973
this.providedTypes.push(type);
8074

81-
const result: string[] = [ this.toTypingString(type.name)];
75+
const result: string[] = [ this.toTypingString(type.name, this.fromTypingImport) ];
8276
let label = result[0];
8377

8478
if (type.category === TypeCategory.Collection) {
@@ -97,7 +91,7 @@ export class TypingHintProvider {
9791
if (this.typeContainer[elementType].name === PythonType.Dict) {
9892
dictElementFound = true;
9993
}
100-
label += this.toTypingString(elementType);
94+
label += this.toTypingString(elementType, this.fromTypingImport);
10195
elementValue = elementValue.trim().substr(1);
10296
elementType = TypeSearch.detectType(elementValue);
10397
collectionCount++;
@@ -129,18 +123,33 @@ export class TypingHintProvider {
129123
* @returns An array of types.
130124
*/
131125
public getRemainingHints(): string[] {
126+
if (!this.typingImportDetected) {
127+
return this.hintsForAllCollectionTypes();
128+
}
132129
const result: string[] = [];
133-
134130
for (const type of Object.values(this.typeContainer).filter(t => t.category === TypeCategory.Collection)) {
135131
if (!this.providedTypes.includes(type)) {
136-
result.push(this.toTypingString(type.name));
132+
result.push(this.toTypingString(type.name, this.fromTypingImport));
137133
}
138134
}
139135
return result;
140136
}
141137

142-
private toTypingString(typeName: string): string {
138+
private hintsForAllCollectionTypes(): string[] {
139+
const firstHalf: string[] = [];
140+
const secondHalf: string[] = [];
141+
for (const type of Object.values(this.typeContainer).filter(t => t.category === TypeCategory.Collection)) {
142+
if (!this.providedTypes.includes(type)) {
143+
const withoutPrefix = this.fromTypingImport || !this.typingImportDetected;
144+
firstHalf.push(this.toTypingString(type.name, withoutPrefix));
145+
secondHalf.push(this.toTypingString(type.name, !withoutPrefix));
146+
}
147+
}
148+
return firstHalf.concat(secondHalf);
149+
}
150+
151+
private toTypingString(typeName: string, withoutPrefix: boolean): string {
143152
const typingName = capitalized(typeName);
144-
return this.fromTypingImport ? `${typingName}[` : `${this.typingPrefix}.${typingName}[`;
153+
return withoutPrefix ? `${typingName}[` : `${this.typingPrefix}.${typingName}[`;
145154
}
146155
}

test/providers/typingHintProvider.test.ts

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,54 @@ suite('TypingHintProvider', () => {
99

1010
const importTyping: string = "import x\nimport typing";
1111
const importTypingAsX: string = "import x\nimport typing as x";
12-
const fromTypingImport: string = "import x\nfrom typing import List";
12+
const fromTypingImport: string = "import x\nfrom typing import List, Dict, Tuple, Set";
1313
const typeContainer = getDataTypeContainer();
1414

1515
suite('containsTyping', () => {
1616

17+
let provider: TypingHintProvider;
18+
19+
setup(() => {
20+
provider = new TypingHintProvider(typeContainer);
21+
});
22+
1723
test("returns true for 'import typing'", async () => {
18-
const provider = new TypingHintProvider(typeContainer);
1924
const actual = await provider.detectTypingImport(importTyping);
2025
assert.equal(actual, true);
2126
});
2227

2328
test("returns true for 'import typing as x'", async () => {
24-
const provider = new TypingHintProvider(typeContainer);
2529
const actual = await provider.detectTypingImport(importTypingAsX);
2630
assert.equal(actual, true);
2731
});
2832

2933
test("returns true for 'from typing import x'", async () => {
30-
const provider = new TypingHintProvider(typeContainer);
3134
const actual = await provider.detectTypingImport(fromTypingImport);
3235
assert.equal(actual, true);
3336
});
3437
});
3538

3639
suite("getHint", () => {
3740

41+
let provider: TypingHintProvider;
42+
43+
setup(() => {
44+
provider = new TypingHintProvider(typeContainer);
45+
});
46+
3847
test("returns typing.Type[", async () => {
39-
const provider = new TypingHintProvider(typeContainer);
4048
const expected = "typing.List[";
41-
providerTest(await provider.detectTypingImport(importTyping), getHintTest, provider, expected);
49+
runTest(await provider.detectTypingImport(importTyping), getHintTest, provider, expected);
4250
});
4351

4452
test("returns x.Type[ for 'import typing as x'", async () => {
45-
const provider = new TypingHintProvider(typeContainer);
4653
const expected = "x.List[";
47-
providerTest(await provider.detectTypingImport(importTypingAsX), getHintTest, provider, expected);
54+
runTest(await provider.detectTypingImport(importTypingAsX), getHintTest, provider, expected);
4855
});
4956

5057
test("returns Type[ for 'from typing' import", async () => {
51-
const provider = new TypingHintProvider(typeContainer);
5258
const expected = "List[";
53-
providerTest(await provider.detectTypingImport(fromTypingImport), getHintTest, provider, expected);
59+
runTest(await provider.detectTypingImport(fromTypingImport), getHintTest, provider, expected);
5460
});
5561

5662
function getHintTest(provider: TypingHintProvider, expected: string) {
@@ -71,55 +77,102 @@ suite('TypingHintProvider', () => {
7177
test("returns Type[ for empty collection", () => {
7278
const data = "[]";
7379
const expected = ["List["];
74-
providerTest(typingImported, getTypingHints, provider, {data, expected }, PythonType.List);
80+
runTest(typingImported, getHintsTest, provider, {data, expected }, PythonType.List);
7581
});
7682

7783
test("returns Dict[ and 'Dict[key,' for dicts", () => {
7884
let data = "{ 1: 2 }";
7985
let expected = ["Dict[", "Dict[int"];
80-
providerTest(typingImported, getTypingHints, provider, { data, expected }, PythonType.Dict);
86+
runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.Dict);
8187
});
8288

8389
test("handles nestled dicts", () => {
8490
let data = "[ { 1: 2 } ]";
8591
let expected = ["List[", "List[Dict[int"];
86-
providerTest(typingImported, getTypingHints, provider, { data, expected }, PythonType.List);
92+
runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.List);
8793
});
8894

8995
test("returns Type[ and Type[type] for non-dicts", () => {
9096
let data = "['str']";
9197
let expected = ["List[", "List[str]"];
92-
providerTest(typingImported, getTypingHints, provider, { data, expected }, PythonType.List);
98+
runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.List);
9399

94100
data = "(1, {'ignore': 'this'})";
95101
expected = ["Tuple[", "Tuple[int]"];
96-
providerTest(typingImported, getTypingHints, provider, { data, expected }, PythonType.Tuple);
102+
runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.Tuple);
97103

98104
data = "{ 1, 2 }";
99105
expected = ["Set[", "Set[int]"];
100-
providerTest(typingImported, getTypingHints, provider, { data, expected }, PythonType.Set);
106+
runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.Set);
101107
});
102108

103109
test("adds typing prefixes for 'import typing' imports", async () => {
104110
let p = new TypingHintProvider(typeContainer);
105111
let data = "[ { 1: 2 } ]";
106112
let expected = ["typing.List[", "typing.List[typing.Dict[int"];
107-
providerTest(
113+
runTest(
108114
await p.detectTypingImport(importTyping),
109-
getTypingHints,
115+
getHintsTest,
110116
p,
111117
{ data, expected },
112118
PythonType.List
113119
);
114120
});
115121

116-
function getTypingHints(provider: TypingHintProvider, testCase: TestCase, type: PythonType) {
122+
function getHintsTest(provider: TypingHintProvider, testCase: TestCase, type: PythonType) {
117123
const actual = provider.getHints(varSearchResult(type, testCase.data));
118124
assert.deepEqual(actual, testCase.expected, messageFor(testCase.data, testCase.expected, actual));
119125
}
120126
});
121127

122-
function providerTest(typingDetected: boolean, test: (...params: any) => void, ...args: any) {
128+
suite("getRemainingHints", () => {
129+
130+
let provider: TypingHintProvider;
131+
132+
setup(() => {
133+
provider = new TypingHintProvider(typeContainer);
134+
});
135+
136+
test("returns all typing hints if typing is not imported, without prefix followed by with prefix", async () => {
137+
await provider.detectTypingImport("");
138+
const expected = typingTypes(false).concat(typingTypes(true));
139+
const actual = provider.getRemainingHints();
140+
assert.deepEqual(actual, expected);
141+
});
142+
143+
test("returns hints without prefix first, for from typing import", async () => {
144+
const expected = typingTypes(false);
145+
runTest(
146+
await provider.detectTypingImport(fromTypingImport),
147+
getRemainingHintsTest,
148+
provider,
149+
expected
150+
);
151+
});
152+
153+
test("returns hints with prefix first, if imported", async () => {
154+
const expected = typingTypes(true);
155+
runTest(
156+
await provider.detectTypingImport(importTyping),
157+
getRemainingHintsTest,
158+
provider,
159+
expected
160+
);
161+
});
162+
163+
164+
function getRemainingHintsTest(provider: TypingHintProvider, expected: string) {
165+
const actual = provider.getRemainingHints();
166+
assert.deepEqual(actual, expected);
167+
}
168+
});
169+
170+
const typingTypes = (withPrefix: boolean) => {
171+
const prefix = withPrefix ? "typing." : "";
172+
return [`${prefix}Dict[`, `${prefix}List[`, `${prefix}Set[`, `${prefix}Tuple[` ];
173+
};
174+
175+
function runTest(typingDetected: boolean, test: (...params: any) => void, ...args: any) {
123176
if (typingDetected) {
124177
test(...args);
125178
} else {

0 commit comments

Comments
 (0)