test_lazy_loader.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import importlib
  2. import os
  3. import subprocess
  4. import sys
  5. import types
  6. from unittest import mock
  7. import pytest
  8. import lazy_loader as lazy
  9. def test_lazy_import_basics():
  10. math = lazy.load("math")
  11. anything_not_real = lazy.load("anything_not_real")
  12. # Now test that accessing attributes does what it should
  13. assert math.sin(math.pi) == pytest.approx(0, 1e-6)
  14. # poor-mans pytest.raises for testing errors on attribute access
  15. try:
  16. anything_not_real.pi
  17. raise AssertionError() # Should not get here
  18. except ModuleNotFoundError:
  19. pass
  20. assert isinstance(anything_not_real, lazy.DelayedImportErrorModule)
  21. # see if it changes for second access
  22. try:
  23. anything_not_real.pi
  24. raise AssertionError() # Should not get here
  25. except ModuleNotFoundError:
  26. pass
  27. def test_lazy_import_subpackages():
  28. with pytest.warns(RuntimeWarning):
  29. hp = lazy.load("html.parser")
  30. assert "html" in sys.modules
  31. assert type(sys.modules["html"]) == type(pytest)
  32. assert isinstance(hp, importlib.util._LazyModule)
  33. assert "html.parser" in sys.modules
  34. assert sys.modules["html.parser"] == hp
  35. def test_lazy_import_impact_on_sys_modules():
  36. math = lazy.load("math")
  37. anything_not_real = lazy.load("anything_not_real")
  38. assert isinstance(math, types.ModuleType)
  39. assert "math" in sys.modules
  40. assert isinstance(anything_not_real, lazy.DelayedImportErrorModule)
  41. assert "anything_not_real" not in sys.modules
  42. # only do this if numpy is installed
  43. pytest.importorskip("numpy")
  44. np = lazy.load("numpy")
  45. assert isinstance(np, types.ModuleType)
  46. assert "numpy" in sys.modules
  47. np.pi # trigger load of numpy
  48. assert isinstance(np, types.ModuleType)
  49. assert "numpy" in sys.modules
  50. def test_lazy_import_nonbuiltins():
  51. np = lazy.load("numpy")
  52. sp = lazy.load("scipy")
  53. if not isinstance(np, lazy.DelayedImportErrorModule):
  54. assert np.sin(np.pi) == pytest.approx(0, 1e-6)
  55. if isinstance(sp, lazy.DelayedImportErrorModule):
  56. try:
  57. sp.pi
  58. raise AssertionError()
  59. except ModuleNotFoundError:
  60. pass
  61. def test_lazy_attach():
  62. name = "mymod"
  63. submods = ["mysubmodule", "anothersubmodule"]
  64. myall = {"not_real_submod": ["some_var_or_func"]}
  65. locls = {
  66. "attach": lazy.attach,
  67. "name": name,
  68. "submods": submods,
  69. "myall": myall,
  70. }
  71. s = "__getattr__, __lazy_dir__, __all__ = attach(name, submods, myall)"
  72. exec(s, {}, locls)
  73. expected = {
  74. "attach": lazy.attach,
  75. "name": name,
  76. "submods": submods,
  77. "myall": myall,
  78. "__getattr__": None,
  79. "__lazy_dir__": None,
  80. "__all__": None,
  81. }
  82. assert locls.keys() == expected.keys()
  83. for k, v in expected.items():
  84. if v is not None:
  85. assert locls[k] == v
  86. def test_attach_same_module_and_attr_name():
  87. from lazy_loader.tests import fake_pkg
  88. # Grab attribute twice, to ensure that importing it does not
  89. # override function by module
  90. assert isinstance(fake_pkg.some_func, types.FunctionType)
  91. assert isinstance(fake_pkg.some_func, types.FunctionType)
  92. # Ensure imports from submodule still work
  93. from lazy_loader.tests.fake_pkg.some_func import some_func
  94. assert isinstance(some_func, types.FunctionType)
  95. FAKE_STUB = """
  96. from . import rank
  97. from ._gaussian import gaussian
  98. from .edges import sobel, scharr, prewitt, roberts
  99. """
  100. def test_stub_loading(tmp_path):
  101. stub = tmp_path / "stub.pyi"
  102. stub.write_text(FAKE_STUB)
  103. _get, _dir, _all = lazy.attach_stub("my_module", str(stub))
  104. expect = {"gaussian", "sobel", "scharr", "prewitt", "roberts", "rank"}
  105. assert set(_dir()) == set(_all) == expect
  106. def test_stub_loading_parity():
  107. from lazy_loader.tests import fake_pkg
  108. from_stub = lazy.attach_stub(fake_pkg.__name__, fake_pkg.__file__)
  109. stub_getter, stub_dir, stub_all = from_stub
  110. assert stub_all == fake_pkg.__all__
  111. assert stub_dir() == fake_pkg.__lazy_dir__()
  112. assert stub_getter("some_func") == fake_pkg.some_func
  113. def test_stub_loading_errors(tmp_path):
  114. stub = tmp_path / "stub.pyi"
  115. stub.write_text("from ..mod import func\n")
  116. with pytest.raises(ValueError, match="Only within-module imports are supported"):
  117. lazy.attach_stub("name", str(stub))
  118. with pytest.raises(ValueError, match="Cannot load imports from non-existent stub"):
  119. lazy.attach_stub("name", "not a file")
  120. stub2 = tmp_path / "stub2.pyi"
  121. stub2.write_text("from .mod import *\n")
  122. with pytest.raises(ValueError, match=".*does not support star import"):
  123. lazy.attach_stub("name", str(stub2))
  124. def test_require_kwarg():
  125. have_importlib_metadata = importlib.util.find_spec("importlib.metadata") is not None
  126. dot = "." if have_importlib_metadata else "_"
  127. # Test with a module that definitely exists, behavior hinges on requirement
  128. with mock.patch(f"importlib{dot}metadata.version") as version:
  129. version.return_value = "1.0.0"
  130. math = lazy.load("math", require="somepkg >= 2.0")
  131. assert isinstance(math, lazy.DelayedImportErrorModule)
  132. math = lazy.load("math", require="somepkg >= 1.0")
  133. assert math.sin(math.pi) == pytest.approx(0, 1e-6)
  134. # We can fail even after a successful import
  135. math = lazy.load("math", require="somepkg >= 2.0")
  136. assert isinstance(math, lazy.DelayedImportErrorModule)
  137. # When a module can be loaded but the version can't be checked,
  138. # raise a ValueError
  139. with pytest.raises(ValueError):
  140. lazy.load("math", require="somepkg >= 1.0")
  141. def test_parallel_load():
  142. pytest.importorskip("numpy")
  143. subprocess.run(
  144. [
  145. sys.executable,
  146. os.path.join(os.path.dirname(__file__), "import_np_parallel.py"),
  147. ]
  148. )