Skip to content

Commit 3632377

Browse files
committed
Add test coverage for lazy imports
1 parent 0cefbcb commit 3632377

9 files changed

Lines changed: 165 additions & 1 deletion

File tree

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ latest
77

88
* Fix panic error when scanning imports of nonexistent namespace children
99
(https://github.com/python-grimp/grimp/issues/308).
10-
* Officially support freethreaded Python 3.14.
10+
* Officially support freethreaded Python 3.14+.
11+
* Officially support Python 3.15, including lazy imports.
1112

1213
3.15 (2026-07-03)
1314
-----------------

tests/assets/lazyimports/__init__.py

Whitespace-only changes.

tests/assets/lazyimports/one.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
lazy import lazyimports.two.blue
2+
lazy from lazyimports import two
3+
if TYPE_CHECKING:
4+
lazy import lazyimports.two.green
5+
lazy from lazyimports.three import SOME_CONSTANT

tests/assets/lazyimports/three.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SOME_CONSTANT = "some-constant"

tests/assets/lazyimports/two/__init__.py

Whitespace-only changes.

tests/assets/lazyimports/two/blue.py

Whitespace-only changes.

tests/assets/lazyimports/two/green.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import grimp
2+
3+
4+
def test_build_graph_with_lazy_imports():
5+
"""
6+
Tests we can cope with lazy imports (Python 3.15+).
7+
8+
Under the hood we use ruff's parser, which understands lazy imports even
9+
when this is running under a different Python version.
10+
"""
11+
graph = grimp.build_graph("lazyimports", cache_dir=None)
12+
13+
result = graph.find_modules_directly_imported_by("lazyimports.one")
14+
15+
assert {
16+
"lazyimports.three",
17+
"lazyimports.two",
18+
"lazyimports.two.blue",
19+
"lazyimports.two.green",
20+
} == result

tests/unit/application/test_scanning.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,143 @@ def test_absolute_imports(include_external_packages, expected_result):
8181
assert {module_file_to_scan: expected_result} == result
8282

8383

84+
def test_lazy_imports():
85+
module_file_to_scan = _module_to_module_file(Module("foo.one.blue"))
86+
all_modules = {
87+
Module("foo"),
88+
Module("foo.one"),
89+
Module("foo.one.blue"),
90+
Module("foo.one.green"),
91+
Module("foo.two"),
92+
Module("foo.two.brown"),
93+
Module("foo.two.yellow"),
94+
Module("foo.three"),
95+
}
96+
file_system = rust.FakeBasicFileSystem(
97+
contents="""
98+
/path/to/foo/
99+
__init__.py
100+
one/
101+
__init__.py
102+
blue.py
103+
green.py
104+
two/
105+
__init__.py
106+
brown.py
107+
yellow.py
108+
three.py
109+
""",
110+
content_map={
111+
"/path/to/foo/one/blue.py": """
112+
# Absolute no-from
113+
lazy import foo.two
114+
lazy import externalone
115+
lazy import externaltwo.subpackage # with comment afterwards.
116+
arbitrary_expression = 1
117+
118+
# Absolute from
119+
lazy from foo.one import green
120+
lazy from foo.two import yellow
121+
if t.TYPE_CHECKING:
122+
lazy from foo import three
123+
lazy from external import one
124+
lazy from external.two import blue # with comment afterwards.
125+
126+
# Relative from
127+
lazy from . import green
128+
lazy from ..two import yellow
129+
lazy from .. import three
130+
"""
131+
}
132+
)
133+
134+
with override_settings(FILE_SYSTEM=file_system):
135+
result = scanning.scan_imports(
136+
{module_file_to_scan},
137+
found_packages={
138+
FoundPackage(
139+
name="foo",
140+
directory="/path/to/foo",
141+
module_files=_modules_to_module_files(all_modules),
142+
)
143+
},
144+
include_external_packages=True,
145+
exclude_type_checking_imports=False,
146+
)
147+
148+
expected = {
149+
module_file_to_scan: {
150+
DirectImport(
151+
importer=Module("foo.one.blue"),
152+
imported=Module("foo.two"),
153+
line_number=2,
154+
line_contents="lazy import foo.two",
155+
),
156+
DirectImport(
157+
importer=Module("foo.one.blue"),
158+
imported=Module("externalone"),
159+
line_number=3,
160+
line_contents="lazy import externalone",
161+
),
162+
DirectImport(
163+
importer=Module("foo.one.blue"),
164+
imported=Module("externaltwo"),
165+
line_number=4,
166+
line_contents="lazy import externaltwo.subpackage # with comment afterwards.",
167+
),
168+
DirectImport(
169+
importer=Module("foo.one.blue"),
170+
imported=Module("foo.one.green"),
171+
line_number=8,
172+
line_contents="lazy from foo.one import green",
173+
),
174+
DirectImport(
175+
importer=Module("foo.one.blue"),
176+
imported=Module("foo.two.yellow"),
177+
line_number=9,
178+
line_contents="lazy from foo.two import yellow",
179+
),
180+
DirectImport(
181+
importer=Module("foo.one.blue"),
182+
imported=Module("foo.three"),
183+
line_number=11,
184+
line_contents="lazy from foo import three",
185+
),
186+
DirectImport(
187+
importer=Module("foo.one.blue"),
188+
imported=Module("external"),
189+
line_number=12,
190+
line_contents="lazy from external import one",
191+
),
192+
DirectImport(
193+
importer=Module("foo.one.blue"),
194+
imported=Module("external"),
195+
line_number=13,
196+
line_contents="lazy from external.two import blue # with comment afterwards.",
197+
),
198+
DirectImport(
199+
importer=Module("foo.one.blue"),
200+
imported=Module("foo.one.green"),
201+
line_number=16,
202+
line_contents="lazy from . import green",
203+
),
204+
DirectImport(
205+
importer=Module("foo.one.blue"),
206+
imported=Module("foo.two.yellow"),
207+
line_number=17,
208+
line_contents="lazy from ..two import yellow",
209+
),
210+
DirectImport(
211+
importer=Module("foo.one.blue"),
212+
imported=Module("foo.three"),
213+
line_number=18,
214+
line_contents="lazy from .. import three",
215+
),
216+
}
217+
}
218+
assert result == expected
219+
220+
84221
def test_non_ascii():
85222
blue_module = Module("mypackage.blue")
86223
blue_module_file = _module_to_module_file(blue_module)

0 commit comments

Comments
 (0)