-
Notifications
You must be signed in to change notification settings - Fork 2
fix: lazy import utilities #814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wyzula-jan
wants to merge
1
commit into
main
Choose a base branch
from
fix/lazy-import-utils
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+151
−19
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,26 @@ | ||
| from bec_lib.utils.rpc_utils import user_access | ||
| from bec_lib.utils.scan_utils import scan_to_csv, scan_to_dict | ||
| from bec_lib.utils.threading_utils import threadlocked | ||
| from importlib import import_module | ||
| from typing import Any | ||
|
|
||
| _LAZY_EXPORTS = { | ||
| "lazy_import": ("bec_lib.utils.import_utils", "lazy_import"), | ||
| "lazy_import_from": ("bec_lib.utils.import_utils", "lazy_import_from"), | ||
| "scan_to_csv": ("bec_lib.utils.scan_utils", "scan_to_csv"), | ||
| "scan_to_dict": ("bec_lib.utils.scan_utils", "scan_to_dict"), | ||
| "threadlocked": ("bec_lib.utils.threading_utils", "threadlocked"), | ||
| "user_access": ("bec_lib.utils.rpc_utils", "user_access"), | ||
| } | ||
|
|
||
| __all__ = sorted(_LAZY_EXPORTS) | ||
|
|
||
|
|
||
| def __getattr__(name: str) -> Any: | ||
| if name not in _LAZY_EXPORTS: | ||
| raise AttributeError(f"module {__name__!r} has no attribute {name!r}") | ||
| module_name, attr_name = _LAZY_EXPORTS[name] | ||
| value = getattr(import_module(module_name), attr_name) | ||
| globals()[name] = value | ||
| return value | ||
|
|
||
|
|
||
| def __dir__() -> list[str]: | ||
| return sorted(set(globals()) | set(__all__)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,40 @@ | ||
| import inspect | ||
| import sys | ||
| from collections.abc import Iterable | ||
| from importlib import import_module | ||
| from typing import Any | ||
| from typing import Any, overload | ||
|
|
||
| from bec_lib.utils.proxy import Proxy | ||
|
|
||
|
|
||
| def lazy_import(module_name): | ||
| def lazy_import(module_name: str) -> Proxy: | ||
| return Proxy(lambda: import_module(module_name), init_once=True) | ||
|
|
||
|
|
||
| def lazy_import_from(module_name, from_list): | ||
| ret = (Proxy(lambda name=name: getattr(import_module(module_name), name)) for name in from_list) | ||
| if len(from_list) == 1: | ||
| return next(ret) | ||
| else: | ||
| return ret | ||
| @overload | ||
| def lazy_import_from(module_name: str, from_list: str) -> Proxy: ... | ||
|
|
||
|
|
||
| def isinstance_based_on_class_name(obj: Any, full_class_name: str): | ||
| @overload | ||
| def lazy_import_from(module_name: str, from_list: Iterable[str]) -> tuple[Proxy, ...] | Proxy: ... | ||
|
|
||
|
|
||
| def lazy_import_from(module_name: str, from_list: str | Iterable[str]) -> tuple[Proxy, ...] | Proxy: | ||
| names = (from_list,) if isinstance(from_list, str) else tuple(from_list) | ||
| proxies = tuple( | ||
| Proxy(lambda name=name: getattr(import_module(module_name), name), init_once=True) | ||
| for name in names | ||
| ) | ||
| if len(proxies) == 1: | ||
| return proxies[0] | ||
| return proxies | ||
|
|
||
|
|
||
| def isinstance_based_on_class_name(obj: Any, full_class_name: str) -> bool: | ||
| """Return if object 'obj' is an instance of class named 'full_class_name' | ||
|
|
||
| 'full_class_name' must be a string like 'class_module.class_name', the corresponding class does not need to be imported at the caller module level | ||
| """ | ||
| import inspect | ||
|
|
||
| return full_class_name in [ | ||
| f"{klass.__module__}.{klass.__name__}" for klass in inspect.getmro(type(obj)) | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,106 @@ | ||
| from bec_lib.device import DeviceBase | ||
| from bec_lib.utils.import_utils import isinstance_based_on_class_name | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| from types import SimpleNamespace | ||
|
|
||
| from bec_lib.utils import import_utils | ||
|
wyzula-jan marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class DummyBase: | ||
| pass | ||
|
|
||
|
|
||
| class DummyChild(DummyBase): | ||
| pass | ||
|
|
||
|
|
||
| def _clean_pythonpath() -> str: | ||
| return os.pathsep.join(str(path) for path in sys.path if path) | ||
|
|
||
|
|
||
| def test_isinstance_based_on_class_name(): | ||
| obj = DeviceBase(name="test_obj") | ||
| obj = DummyChild() | ||
| assert import_utils.isinstance_based_on_class_name( | ||
| obj, f"{DummyBase.__module__}.{DummyBase.__name__}" | ||
| ) | ||
| assert import_utils.isinstance_based_on_class_name( | ||
| obj, f"{DummyChild.__module__}.{DummyChild.__name__}" | ||
| ) | ||
| assert not import_utils.isinstance_based_on_class_name(obj, "builtins.dict") | ||
|
|
||
|
|
||
| def test_lazy_import_from_accepts_string_input(): | ||
| json_decoder = import_utils.lazy_import_from("json", "JSONDecoder") | ||
| assert json_decoder.__name__ == "JSONDecoder" | ||
|
|
||
|
|
||
| def test_lazy_import_from_single_tuple_returns_single_proxy(): | ||
| json_decoder = import_utils.lazy_import_from("json", ("JSONDecoder",)) | ||
| assert json_decoder.__name__ == "JSONDecoder" | ||
|
|
||
|
|
||
| def test_lazy_import_from_multiple_names_returns_tuple(): | ||
| proxies = import_utils.lazy_import_from("json", ("JSONDecoder", "JSONEncoder")) | ||
| assert isinstance(proxies, tuple) | ||
| assert [proxy.__name__ for proxy in proxies] == ["JSONDecoder", "JSONEncoder"] | ||
|
|
||
|
|
||
| def test_lazy_import_from_materializes_once(monkeypatch): | ||
| calls = [] | ||
|
|
||
| def fake_import(module_name): | ||
| calls.append(module_name) | ||
| return SimpleNamespace(DemoClass=type("DemoClass", (), {})) | ||
|
|
||
| monkeypatch.setattr(import_utils, "import_module", fake_import) | ||
|
|
||
| demo_class = import_utils.lazy_import_from("demo.module", "DemoClass") | ||
| assert demo_class.__name__ == "DemoClass" | ||
| assert demo_class.__name__ == "DemoClass" | ||
| assert calls == ["demo.module"] | ||
|
|
||
|
|
||
| def test_lazy_import_does_not_import_module_until_use(tmp_path, monkeypatch): | ||
| module_name = "lazy_target_module" | ||
| module_path = tmp_path / "lazy_target_module.py" | ||
| module_path.write_text("VALUE = 123\n", encoding="utf-8") | ||
| monkeypatch.syspath_prepend(str(tmp_path)) | ||
| sys.modules.pop(module_name, None) | ||
|
|
||
| mod = import_utils.lazy_import(module_name) | ||
|
|
||
| assert module_name not in sys.modules | ||
| assert mod.VALUE == 123 | ||
| assert module_name in sys.modules | ||
|
|
||
|
|
||
| def test_lazy_import_from_does_not_import_module_until_use(tmp_path, monkeypatch): | ||
| module_name = "lazy_from_target_module" | ||
| module_path = tmp_path / "lazy_from_target_module.py" | ||
| module_path.write_text("class DemoClass:\n" " VALUE = 456\n", encoding="utf-8") | ||
| monkeypatch.syspath_prepend(str(tmp_path)) | ||
| sys.modules.pop(module_name, None) | ||
|
|
||
| demo_cls = import_utils.lazy_import_from(module_name, "DemoClass") | ||
|
|
||
| assert module_name not in sys.modules | ||
| assert demo_cls.VALUE == 456 | ||
| assert module_name in sys.modules | ||
|
|
||
|
|
||
| assert isinstance_based_on_class_name(obj, "bec_lib.device.DeviceBase") | ||
| assert not isinstance_based_on_class_name(obj, "bec_lib.device.Status") | ||
| def test_importing_import_utils_does_not_import_scan_utils(): | ||
| # This needs a clean interpreter because sys.modules is shared by the test process. | ||
| env = os.environ | {"PYTHONPATH": _clean_pythonpath()} | ||
| proc = subprocess.run( | ||
| [ | ||
| sys.executable, | ||
| "-c", | ||
| "from bec_lib.utils.import_utils import lazy_import_from; import sys; " | ||
| "print('bec_lib.utils.scan_utils' in sys.modules)", | ||
| ], | ||
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| env=env, | ||
| ) | ||
| assert proc.stdout.strip() == "False" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.