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
« 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
12from inline_snapshot import snapshot
14python_version = f"python{sys.version_info[0]}.{sys.version_info[1]}"
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)
24@contextmanager
25def package(name, content, extra_config=""):
26 content = {
27 "pyproject.toml": f"""
29[build-system]
30requires = ["hatchling"]
31build-backend = "hatchling.build"
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()
45 write_files(package_dir, content)
47 subprocess.run(
48 [sys.executable, "-m", "pip", "install", str(package_dir)],
49 input=b"y",
50 check=True,
51 )
53 yield
55 subprocess.run(
56 [sys.executable, "-m", "pip", "uninstall", name], input=b"y", check=True
57 )
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")
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 )
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
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]
92 packages = [package("test_pck", p) if isinstance(p, dict) else p for p in packages]
94 with ExitStack() as cm, TemporaryDirectory() as script_dir:
95 for p in packages:
96 cm.enter_context(p)
98 print(sys.exec_prefix)
99 script_dir = Path(script_dir)
101 script_file = script_dir / "script.py"
102 script_file.write_text(script)
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 )
111 transformed_result = sp.run(
112 [sys.executable, str(script_file)], cwd=str(script_dir), capture_output=True
113 )
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)
120 assert normal_stdout == n_stdout
121 assert transformed_stdout == (
122 "<equal to normal>" if n_stdout == t_stdout else t_stdout
123 )
125 assert normal_stderr == n_stderr
126 assert transformed_stderr == (
127 "<equal to normal>" if n_stderr == t_stderr else t_stderr
128 )
131def test_loader():
132 check_script(
133 {
134 "test_pck/__init__.py": """\
135from .mx import x
136from .my import y
138def use_x():
139 return x
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 )
179def test_loader_keywords():
180 check_script(
181 {
182 "test_pck/__init__.py": """\
183from .mx import x
184print("imported init")
186def use_x():
187 return x
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 )
218def test_lazy_module_attr():
219 check_script(
220 {
221 "test_pck/__init__.py": """\
222from .mx import x
223from .my import y
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)
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 )
263def test_lazy_module_content():
264 check_script(
265 {
266 "test_pck/__init__.py": """\
267from .mx import x
268from .my import y
270""",
271 "test_pck/mx.py": """\
272x=5
273""",
274 "test_pck/my.py": """\
275y=5
276""",
277 },
278 """\
279import test_pck
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 )
301def test_lazy_module_content_import_from():
302 check_script(
303 {
304 "test_pck/__init__.py": """\
305from .mx import x
306print("inside",globals().keys())
308try:
309 print("mx",mx)
310except:
311 print("no mx")
313print("inside",globals().keys())
315def later():
316 print("later",globals().keys())
317""",
318 "test_pck/mx.py": """\
319x=5
320""",
321 },
322 """\
323import test_pck
325print("outside",vars(test_pck).keys())
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 )
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 )
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 )
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
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 )
459def test_lazy_module_setattr():
460 check_script(
461 {
462 "test_pck/__init__.py": """\
463from .ma import b
465def foo():
466 print(b())
468""",
469 "test_pck/ma.py": """\
470def b():
471 return 5
472""",
473 },
474 """\
475from test_pck import foo
476import test_pck
478foo()
479test_pck.b=lambda:6
480foo()
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 )
495def test_loader_is_used():
496 check_script(
497 {
498 "test_pck/__init__.py": """\
500def foo():
501 print("foo")
503""",
504 },
505 """\
506import test_pck
508print(type(test_pck.__spec__.loader))
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 )
526def test_different_package_and_project_name():
527 check_script(
528 package(
529 "py-test-pck",
530 {
531 "source/test_pck/__init__.py": """\
533def foo():
534 print("foo")
536""",
537 },
538 extra_config="""
539[tool.hatch.build.targets.wheel]
540packages = ["source/test_pck"]
541""",
542 ),
543 """\
544import test_pck
546print(type(test_pck.__spec__.loader))
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 )