Skip to content

Commit ba33959

Browse files
hakancelikdevclaude
andcommitted
Fix incorrect removal of runtime import shadowed by TYPE_CHECKING import (#313)
When the same name is imported both at runtime and inside an `if TYPE_CHECKING:` block, unimport incorrectly flagged the runtime import as unused because the TYPE_CHECKING import won duplicate resolution (being closer to usage). Since TYPE_CHECKING blocks only execute during static analysis, their imports should never shadow runtime imports. Add _is_type_checking_block() to ImportAnalyzer.visit_If to detect `if TYPE_CHECKING:`, `if typing.TYPE_CHECKING:`, and alias forms like `if t.TYPE_CHECKING:`. When detected, imports in the body are skipped entirely — they are not registered as imports, so they cannot interfere with runtime import analysis. Update existing type_variable test expectations (TYPE_CHECKING-body imports are no longer in IMPORTS list). Add a new test case reproducing the exact scenario from issue #313. Update supported-behaviors.md with TYPE_CHECKING documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a702ffb commit ba33959

File tree

8 files changed

+81
-32
lines changed

8 files changed

+81
-32
lines changed

docs/tutorial/supported-behaviors.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,29 @@ For more information
5555

5656
---
5757

58+
#### TYPE_CHECKING
59+
60+
Unimport recognizes `if TYPE_CHECKING:` blocks and skips imports inside them. These
61+
imports only run during static analysis and are not available at runtime, so they should
62+
never shadow or conflict with runtime imports.
63+
64+
```python
65+
from qtpy import QtCore
66+
import typing as t
67+
68+
if t.TYPE_CHECKING:
69+
from PySide6 import QtCore
70+
71+
class MyThread(QtCore.QThread):
72+
pass
73+
```
74+
75+
In this example, unimport correctly keeps `from qtpy import QtCore` (the runtime import)
76+
and ignores the `TYPE_CHECKING`-guarded import. Both `if TYPE_CHECKING:` and
77+
`if typing.TYPE_CHECKING:` (or any alias like `if t.TYPE_CHECKING:`) are supported.
78+
79+
---
80+
5881
## All
5982

6083
Unimport looks at the items in the `__all__` list, if it matches the imports, marks it

src/unimport/analyzers/import_statement.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,21 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
8484
node=node,
8585
)
8686

87+
@staticmethod
88+
def _is_type_checking_block(if_node: ast.If) -> bool:
89+
test = if_node.test
90+
if isinstance(test, ast.Name) and test.id == "TYPE_CHECKING":
91+
return True
92+
if isinstance(test, ast.Attribute) and test.attr == "TYPE_CHECKING":
93+
return True
94+
return False
95+
8796
def visit_If(self, if_node: ast.If) -> None:
97+
if self._is_type_checking_block(if_node):
98+
for node in if_node.orelse:
99+
self.visit(node)
100+
return
101+
88102
self.if_names = {
89103
name.asname or name.name
90104
for n in filter(lambda node: isinstance(node, (ast.Import, ast.ImportFrom)), if_node.body)

tests/cases/analyzer/type_variable/type_assing_cast.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,5 @@
2121
star=False,
2222
suggestions=[],
2323
),
24-
ImportFrom(
25-
lineno=4,
26-
column=1,
27-
name="QtWebKit",
28-
package="PyQt5",
29-
star=False,
30-
suggestions=[],
31-
),
3224
]
3325
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []

tests/cases/analyzer/type_variable/type_assing_list.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,5 @@
2828
star=False,
2929
suggestions=[],
3030
),
31-
ImportFrom(
32-
lineno=4,
33-
column=1,
34-
name="QWebHistory",
35-
package="PyQt5.QtWebKit",
36-
star=False,
37-
suggestions=[],
38-
),
3931
]
4032
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []

tests/cases/analyzer/type_variable/type_assing_union.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,5 @@
2929
star=False,
3030
suggestions=[],
3131
),
32-
ImportFrom(
33-
lineno=4,
34-
column=1,
35-
name="QtWebEngineWidgets",
36-
package="PyQt5",
37-
star=False,
38-
suggestions=[],
39-
),
40-
ImportFrom(
41-
lineno=4,
42-
column=2,
43-
name="QtWebKit",
44-
package="PyQt5",
45-
star=False,
46-
suggestions=[],
47-
),
4832
]
4933
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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="t.TYPE_CHECKING", is_all=False),
10+
Name(lineno=7, name="QtCore.QThread", is_all=False),
11+
]
12+
IMPORTS: list[Union[Import, ImportFrom]] = [
13+
ImportFrom(
14+
lineno=1,
15+
column=1,
16+
name="QtCore",
17+
package="qtpy",
18+
star=False,
19+
suggestions=[],
20+
),
21+
Import(
22+
lineno=2,
23+
column=1,
24+
name="t",
25+
package="typing",
26+
),
27+
]
28+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from qtpy import QtCore
2+
import typing as t
3+
4+
if t.TYPE_CHECKING:
5+
from PySide6 import QtCore
6+
7+
class MyThread(QtCore.QThread):
8+
pass
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from qtpy import QtCore
2+
import typing as t
3+
4+
if t.TYPE_CHECKING:
5+
from PySide6 import QtCore
6+
7+
class MyThread(QtCore.QThread):
8+
pass

0 commit comments

Comments
 (0)