Skip to content

Commit fc46bc7

Browse files
hakancelikdevclaude
andcommitted
Extend dedup to handle star+explicit clashes and add edge case tests
Extend _deduplicate_star_suggestions() to also consider explicit (non-star) imports in the seen set, preventing star imports from suggesting names that already have an explicit import. This fixes duplicate output like: from json import JSONEncoder (from star expansion) from json import JSONEncoder (from explicit import) Add two new edge case test cases: - partial_overlap_star_imports: reverse order where each star import has both shared and unique names, verifying unique names are preserved - star_import_with_explicit: star import + explicit import of same name, verifying star doesn't duplicate the explicit import Update existing star_imports and 2 test expectations to reflect the fix (json star import no longer suggests JSONEncoder when explicit import exists). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d2c200 commit fc46bc7

File tree

11 files changed

+119
-9
lines changed

11 files changed

+119
-9
lines changed

src/unimport/analyzers/main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,19 @@ def skip_file(self) -> bool:
5555

5656
@staticmethod
5757
def _deduplicate_star_suggestions() -> None:
58-
"""Remove duplicate suggestions across star imports.
58+
"""Remove duplicate suggestions across star and explicit imports.
5959
60-
When multiple star imports export the same name, the last one wins
61-
(matching Python's shadowing semantics).
60+
When multiple imports provide the same name, the last one wins
61+
(matching Python's shadowing semantics). Explicit imports also
62+
claim their name so star imports don't produce duplicates.
6263
"""
6364
seen: set[str] = set()
6465
for imp in reversed(Import.imports):
6566
if isinstance(imp, ImportFrom) and imp.star:
6667
imp.suggestions = [s for s in imp.suggestions if s not in seen]
6768
seen.update(imp.suggestions)
69+
else:
70+
seen.add(imp.name)
6871

6972
@staticmethod
7073
def clear():

tests/cases/analyzer/star_import/2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
name="json",
9797
package="json",
9898
star=True,
99-
suggestions=["JSONEncoder"],
99+
suggestions=[],
100100
),
101101
ImportFrom(
102102
lineno=14,
@@ -114,7 +114,7 @@
114114
name="json",
115115
package="json",
116116
star=True,
117-
suggestions=["JSONEncoder"],
117+
suggestions=[],
118118
),
119119
ImportFrom(
120120
lineno=1,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=4, name="print", is_all=False),
10+
Name(lineno=4, name="Counter", is_all=False),
11+
Name(lineno=5, name="print", is_all=False),
12+
Name(lineno=5, name="defaultdict", is_all=False),
13+
Name(lineno=6, name="print", is_all=False),
14+
Name(lineno=6, name="deque", is_all=False),
15+
]
16+
IMPORTS: list[Union[Import, ImportFrom]] = [
17+
ImportFrom(
18+
lineno=1,
19+
column=1,
20+
name="collections",
21+
package="collections",
22+
star=True,
23+
suggestions=["Counter"],
24+
),
25+
ImportFrom(
26+
lineno=2,
27+
column=1,
28+
name="_collections",
29+
package="_collections",
30+
star=True,
31+
suggestions=["defaultdict", "deque"],
32+
),
33+
]
34+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = [
35+
ImportFrom(
36+
lineno=2,
37+
column=1,
38+
name="_collections",
39+
package="_collections",
40+
star=True,
41+
suggestions=["defaultdict", "deque"],
42+
),
43+
ImportFrom(
44+
lineno=1,
45+
column=1,
46+
name="collections",
47+
package="collections",
48+
star=True,
49+
suggestions=["Counter"],
50+
),
51+
]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=4, name="print", is_all=False),
10+
Name(lineno=4, name="defaultdict", is_all=False),
11+
]
12+
IMPORTS: list[Union[Import, ImportFrom]] = [
13+
ImportFrom(
14+
lineno=1,
15+
column=1,
16+
name="_collections",
17+
package="_collections",
18+
star=True,
19+
suggestions=[],
20+
),
21+
ImportFrom(
22+
lineno=2,
23+
column=1,
24+
name="defaultdict",
25+
package="collections",
26+
star=False,
27+
suggestions=[],
28+
),
29+
]
30+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = [
31+
ImportFrom(
32+
lineno=1,
33+
column=1,
34+
name="_collections",
35+
package="_collections",
36+
star=True,
37+
suggestions=[],
38+
),
39+
]

tests/cases/analyzer/star_import/star_imports.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
name="json",
5353
package="json",
5454
star=True,
55-
suggestions=["JSONEncoder"],
55+
suggestions=[],
5656
),
5757
ImportFrom(
5858
lineno=6,
@@ -70,7 +70,7 @@
7070
name="json",
7171
package="json",
7272
star=True,
73-
suggestions=["JSONEncoder"],
73+
suggestions=[],
7474
),
7575
ImportFrom(
7676
lineno=4,
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from json import JSONEncoder
2-
from json import JSONEncoder
32

43

54
print(JSONEncoder)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from collections import Counter
2+
from _collections import defaultdict, deque
3+
4+
print(Counter)
5+
print(defaultdict)
6+
print(deque)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from collections import defaultdict
2+
3+
print(defaultdict)

tests/cases/refactor/star_import/star_imports.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from re import match, search
22
from json import JSONEncoder
3-
from json import JSONEncoder
43

54

65
print(match)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from collections import *
2+
from _collections import *
3+
4+
print(Counter)
5+
print(defaultdict)
6+
print(deque)

0 commit comments

Comments
 (0)