Coverage for tests/test_loader.py: 99%

83 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-11 15:02 +0100

1import os 

2import re 

3import subprocess 

4import subprocess as sp 

5import sys 

6import typing 

7from contextlib import contextmanager 

8from contextlib import ExitStack 

9from pathlib import Path 

10from tempfile import TemporaryDirectory 

11 

12from inline_snapshot import snapshot 

13 

14python_version = f"python{sys.version_info[0]}.{sys.version_info[1]}" 

15 

16 

17def write_files(dir, content): 

18 for path, text in content.items(): 

19 path = dir / path 

20 path.parent.mkdir(exist_ok=True, parents=True) 

21 path.write_text(text) 

22 

23 

24@contextmanager 

25def package(name, content, extra_config=""): 

26 content = { 

27 "pyproject.toml": f""" 

28 

29[build-system] 

30requires = ["hatchling"] 

31build-backend = "hatchling.build" 

32 

33[project] 

34name="{name}" 

35keywords=["lazy-imports-lite-enabled"] 

36version="0.0.1" 

37""" 

38 + extra_config, 

39 **content, 

40 } 

41 with TemporaryDirectory() as d: 

42 package_dir = Path(d) / name 

43 package_dir.mkdir() 

44 

45 write_files(package_dir, content) 

46 

47 subprocess.run( 

48 [sys.executable, "-m", "pip", "install", str(package_dir)], 

49 input=b"y", 

50 check=True, 

51 ) 

52 

53 yield 

54 

55 subprocess.run( 

56 [sys.executable, "-m", "pip", "uninstall", name], input=b"y", check=True 

57 ) 

58 

59 

60def check_script( 

61 packages, 

62 script, 

63 *, 

64 transformed_stdout="", 

65 transformed_stderr="", 

66 normal_stdout="", 

67 normal_stderr="", 

68): 

69 def normalize_output(output: bytes): 

70 text = output.decode() 

71 text = text.replace("\r\n", "\n") 

72 

73 prefix = re.escape(sys.exec_prefix.replace("\\", "\\\\")) 

74 backslash = "\\" 

75 text = re.sub( 

76 f"'{prefix}[^']*site-packages([^']*)'", 

77 lambda m: f"'<exec_prefix>{typing.cast(str,m[1]).replace(backslash*2,'/')}'", 

78 text, 

79 ) 

80 

81 text = re.sub("at 0x[0-9a-fA-F]*>", "at <hex_value>>", text) 

82 text = re.sub("line [0-9]*", "line <n>", text) 

83 text = text.replace(python_version, "<python_version>") 

84 text = text.replace(str(script_dir), "<script_dir>") 

85 if " \n" in text: 

86 text = text.replace("\n", "⏎\n") 

87 return text 

88 

89 if not isinstance(packages, list): 89 ↛ 92line 89 didn't jump to line 92, because the condition on line 89 was never false

90 packages = [packages] 

91 

92 packages = [package("test_pck", p) if isinstance(p, dict) else p for p in packages] 

93 

94 with ExitStack() as cm, TemporaryDirectory() as script_dir: 

95 for p in packages: 

96 cm.enter_context(p) 

97 

98 print(sys.exec_prefix) 

99 script_dir = Path(script_dir) 

100 

101 script_file = script_dir / "script.py" 

102 script_file.write_text(script) 

103 

104 normal_result = sp.run( 

105 [sys.executable, str(script_file)], 

106 cwd=str(script_dir), 

107 env={**os.environ, "LAZY_IMPORTS_LITE_DISABLE": "True"}, 

108 capture_output=True, 

109 ) 

110 

111 transformed_result = sp.run( 

112 [sys.executable, str(script_file)], cwd=str(script_dir), capture_output=True 

113 ) 

114 

115 n_stdout = normalize_output(normal_result.stdout) 

116 t_stdout = normalize_output(transformed_result.stdout) 

117 n_stderr = normalize_output(normal_result.stderr) 

118 t_stderr = normalize_output(transformed_result.stderr) 

119 

120 assert normal_stdout == n_stdout 

121 assert transformed_stdout == ( 

122 "<equal to normal>" if n_stdout == t_stdout else t_stdout 

123 ) 

124 

125 assert normal_stderr == n_stderr 

126 assert transformed_stderr == ( 

127 "<equal to normal>" if n_stderr == t_stderr else t_stderr 

128 ) 

129 

130 

131def test_loader(): 

132 check_script( 

133 { 

134 "test_pck/__init__.py": """\ 

135from .mx import x 

136from .my import y 

137 

138def use_x(): 

139 return x 

140 

141def use_y(): 

142 return y 

143""", 

144 "test_pck/mx.py": """\ 

145print('imported mx') 

146x=5 

147""", 

148 "test_pck/my.py": """\ 

149print('imported my') 

150y=5 

151""", 

152 }, 

153 """\ 

154from test_pck import use_x, use_y 

155print("y:",use_y()) 

156print("x:",use_x()) 

157""", 

158 transformed_stdout=snapshot( 

159 """\ 

160imported my 

161y: 5 

162imported mx 

163x: 5 

164""" 

165 ), 

166 transformed_stderr=snapshot("<equal to normal>"), 

167 normal_stdout=snapshot( 

168 """\ 

169imported mx 

170imported my 

171y: 5 

172x: 5 

173""" 

174 ), 

175 normal_stderr=snapshot(""), 

176 ) 

177 

178 

179def test_loader_keywords(): 

180 check_script( 

181 { 

182 "test_pck/__init__.py": """\ 

183from .mx import x 

184print("imported init") 

185 

186def use_x(): 

187 return x 

188 

189""", 

190 "test_pck/mx.py": """\ 

191print('imported mx') 

192x=5 

193""", 

194 }, 

195 """\ 

196from test_pck import use_x 

197print("x:",use_x()) 

198""", 

199 transformed_stdout=snapshot( 

200 """\ 

201imported init 

202imported mx 

203x: 5 

204""" 

205 ), 

206 transformed_stderr=snapshot("<equal to normal>"), 

207 normal_stdout=snapshot( 

208 """\ 

209imported mx 

210imported init 

211x: 5 

212""" 

213 ), 

214 normal_stderr=snapshot(""), 

215 ) 

216 

217 

218def test_lazy_module_attr(): 

219 check_script( 

220 { 

221 "test_pck/__init__.py": """\ 

222from .mx import x 

223from .my import y 

224 

225""", 

226 "test_pck/mx.py": """\ 

227print('imported mx') 

228x=5 

229""", 

230 "test_pck/my.py": """\ 

231print('imported my') 

232y=5 

233""", 

234 }, 

235 """\ 

236from test_pck import y 

237print("y:",y) 

238 

239from test_pck import x 

240print("x:",x) 

241""", 

242 transformed_stdout=snapshot( 

243 """\ 

244imported my 

245y: 5 

246imported mx 

247x: 5 

248""" 

249 ), 

250 transformed_stderr=snapshot("<equal to normal>"), 

251 normal_stdout=snapshot( 

252 """\ 

253imported mx 

254imported my 

255y: 5 

256x: 5 

257""" 

258 ), 

259 normal_stderr=snapshot(""), 

260 ) 

261 

262 

263def test_lazy_module_content(): 

264 check_script( 

265 { 

266 "test_pck/__init__.py": """\ 

267from .mx import x 

268from .my import y 

269 

270""", 

271 "test_pck/mx.py": """\ 

272x=5 

273""", 

274 "test_pck/my.py": """\ 

275y=5 

276""", 

277 }, 

278 """\ 

279import test_pck 

280 

281print(test_pck) 

282print(vars(test_pck).keys()) 

283""", 

284 transformed_stdout=snapshot( 

285 """\ 

286<module 'test_pck' from '<exec_prefix>/test_pck/__init__.py'> 

287dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'y']) 

288""" 

289 ), 

290 transformed_stderr=snapshot("<equal to normal>"), 

291 normal_stdout=snapshot( 

292 """\ 

293<module 'test_pck' from '<exec_prefix>/test_pck/__init__.py'> 

294dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'my', 'y']) 

295""" 

296 ), 

297 normal_stderr=snapshot(""), 

298 ) 

299 

300 

301def test_lazy_module_content_import_from(): 

302 check_script( 

303 { 

304 "test_pck/__init__.py": """\ 

305from .mx import x 

306print("inside",globals().keys()) 

307 

308try: 

309 print("mx",mx) 

310except: 

311 print("no mx") 

312 

313print("inside",globals().keys()) 

314 

315def later(): 

316 print("later",globals().keys()) 

317""", 

318 "test_pck/mx.py": """\ 

319x=5 

320""", 

321 }, 

322 """\ 

323import test_pck 

324 

325print("outside",vars(test_pck).keys()) 

326 

327test_pck.later() 

328""", 

329 transformed_stdout=snapshot( 

330 """\ 

331inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x']) 

332mx <module 'test_pck.mx' from '<exec_prefix>/test_pck/mx.py'> 

333inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx']) 

334outside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx', 'later']) 

335later dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'x', 'mx', 'later']) 

336""" 

337 ), 

338 transformed_stderr=snapshot("<equal to normal>"), 

339 normal_stdout=snapshot( 

340 """\ 

341inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x']) 

342mx <module 'test_pck.mx' from '<exec_prefix>/test_pck/mx.py'> 

343inside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x']) 

344outside dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'later']) 

345later dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'mx', 'x', 'later']) 

346""" 

347 ), 

348 normal_stderr=snapshot(""), 

349 ) 

350 

351 

352def test_import_module_with_error(): 

353 check_script( 

354 { 

355 "test_pck/__init__.py": """\ 

356import test_pck.m 

357print(test_pck.m.v) 

358""", 

359 "test_pck/m.py": """\ 

360raise ValueError() 

361""", 

362 }, 

363 """\ 

364try: 

365 from test_pck import v 

366except BaseException as e: 

367 while e: 

368 print(f"{type(e).__name__}: {e}") 

369 e=e.__cause__ if e.__suppress_context__ else e.__context__ 

370""", 

371 transformed_stdout=snapshot( 

372 """\ 

373LazyImportError: Deferred importing of module 'test_pck.m' caused an error⏎ 

374ValueError: ⏎ 

375""" 

376 ), 

377 transformed_stderr=snapshot("<equal to normal>"), 

378 normal_stdout=snapshot( 

379 """\ 

380ValueError: ⏎ 

381""" 

382 ), 

383 normal_stderr=snapshot(""), 

384 ) 

385 

386 

387def test_load_chain_of_modules_with_error(): 

388 check_script( 

389 { 

390 "test_pck/__init__.py": """\ 

391from .m import v 

392print(v) 

393""", 

394 "test_pck/m/__init__.py": """\ 

395from .x import v 

396print(v) 

397""", 

398 "test_pck/m/x.py": """\ 

399from .y import v 

400print(v) 

401""", 

402 "test_pck/m/y.py": """\ 

403raise ValueError() 

404""", 

405 }, 

406 """\ 

407try: 

408 from test_pck import v 

409except BaseException as e: 

410 while e: 

411 print(f"{type(e).__name__}: {e}") 

412 e=e.__cause__ if e.__suppress_context__ else e.__context__ 

413""", 

414 transformed_stdout=snapshot( 

415 """\ 

416LazyImportError: Deferred importing of module '.y' in 'test_pck.m' caused an error⏎ 

417ValueError: ⏎ 

418""" 

419 ), 

420 transformed_stderr=snapshot("<equal to normal>"), 

421 normal_stdout=snapshot( 

422 """\ 

423ValueError: ⏎ 

424""" 

425 ), 

426 normal_stderr=snapshot(""), 

427 ) 

428 

429 

430def test_lazy_module_import_from_empty_init(): 

431 check_script( 

432 { 

433 "test_pck/__init__.py": """\ 

434""", 

435 "test_pck/ma.py": """\ 

436a=5 

437""", 

438 "test_pck/mb.py": """\ 

439from test_pck import ma 

440a=ma.a 

441""", 

442 }, 

443 """\ 

444from test_pck import mb 

445 

446print(mb.a) 

447""", 

448 transformed_stdout=snapshot("<equal to normal>"), 

449 transformed_stderr=snapshot("<equal to normal>"), 

450 normal_stdout=snapshot( 

451 """\ 

4525 

453""" 

454 ), 

455 normal_stderr=snapshot(""), 

456 ) 

457 

458 

459def test_lazy_module_setattr(): 

460 check_script( 

461 { 

462 "test_pck/__init__.py": """\ 

463from .ma import b 

464 

465def foo(): 

466 print(b()) 

467 

468""", 

469 "test_pck/ma.py": """\ 

470def b(): 

471 return 5 

472""", 

473 }, 

474 """\ 

475from test_pck import foo 

476import test_pck 

477 

478foo() 

479test_pck.b=lambda:6 

480foo() 

481 

482""", 

483 transformed_stdout=snapshot("<equal to normal>"), 

484 transformed_stderr=snapshot("<equal to normal>"), 

485 normal_stdout=snapshot( 

486 """\ 

4875 

4886 

489""" 

490 ), 

491 normal_stderr=snapshot(""), 

492 ) 

493 

494 

495def test_loader_is_used(): 

496 check_script( 

497 { 

498 "test_pck/__init__.py": """\ 

499 

500def foo(): 

501 print("foo") 

502 

503""", 

504 }, 

505 """\ 

506import test_pck 

507 

508print(type(test_pck.__spec__.loader)) 

509 

510""", 

511 transformed_stdout=snapshot( 

512 """\ 

513<class 'lazy_imports_lite._loader.LazyLoader'> 

514""" 

515 ), 

516 transformed_stderr=snapshot("<equal to normal>"), 

517 normal_stdout=snapshot( 

518 """\ 

519<class '_frozen_importlib_external.SourceFileLoader'> 

520""" 

521 ), 

522 normal_stderr=snapshot(""), 

523 ) 

524 

525 

526def test_different_package_and_project_name(): 

527 check_script( 

528 package( 

529 "py-test-pck", 

530 { 

531 "source/test_pck/__init__.py": """\ 

532 

533def foo(): 

534 print("foo") 

535 

536""", 

537 }, 

538 extra_config=""" 

539[tool.hatch.build.targets.wheel] 

540packages = ["source/test_pck"] 

541""", 

542 ), 

543 """\ 

544import test_pck 

545 

546print(type(test_pck.__spec__.loader)) 

547 

548""", 

549 transformed_stdout=snapshot( 

550 """\ 

551<class 'lazy_imports_lite._loader.LazyLoader'> 

552""" 

553 ), 

554 transformed_stderr=snapshot("<equal to normal>"), 

555 normal_stdout=snapshot( 

556 """\ 

557<class '_frozen_importlib_external.SourceFileLoader'> 

558""" 

559 ), 

560 normal_stderr=snapshot(""), 

561 )