Coverage for src / ezqt_app / shared / resources / __init__.py: 87.10%

50 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-06 13:12 +0000

1# /////////////////////////////////////////////////////////////// 

2# SHARED.RESOURCES - Shared resource definitions 

3# Project: ezqt_app 

4# /////////////////////////////////////////////////////////////// 

5 

6"""Shared static resource path definitions.""" 

7 

8from __future__ import annotations 

9 

10# /////////////////////////////////////////////////////////////// 

11# IMPORTS 

12# /////////////////////////////////////////////////////////////// 

13import importlib.util 

14import sys 

15from pathlib import Path 

16from types import ModuleType 

17from typing import Any # noqa: F401 # pyright: ignore[reportUnusedImport] 

18 

19# Third-party imports 

20from ezplog.lib_mode import get_logger 

21 

22# Local imports 

23from ...resources import ( 

24 base_resources_rc, # noqa: F401 # pyright: ignore[reportUnusedImport] 

25) 

26from ...utils.runtime_paths import get_bin_path 

27from .icons import Icons as _PackageIcons 

28from .images import Images as _PackageImages 

29 

30Icons = _PackageIcons 

31Images = _PackageImages 

32 

33# Typed accessors for user-compiled resources — populated by load_runtime_rc(). 

34# Typed as Any: attributes are generated dynamically from bin/app_icons.py. 

35# 

36# WARNING — import pattern: 

37# Always access via the module, not via a direct import, to ensure the 

38# reference stays live after load_runtime_rc() reassigns the variable: 

39# 

40# ✅ from ezqt_app.shared import resources; resources.AppIcons.my_icon 

41# ✅ from ezqt_app.shared.resources import AppIcons (only after init()) 

42# ❌ from ezqt_app.shared.resources import AppIcons (at module top-level, 

43# before init() — captures None and never updates) 

44AppIcons: Any = None 

45AppImages: Any = None 

46 

47_LOGGER = get_logger(__name__) 

48 

49 

50def _load_module_from_file(module_name: str, file_path: Path) -> ModuleType | None: 

51 """Import a Python module directly from file when it exists.""" 

52 if not file_path.exists() or not file_path.is_file(): 

53 return None 

54 

55 spec = importlib.util.spec_from_file_location(module_name, file_path) 

56 if spec is None or spec.loader is None: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true

57 return None 

58 

59 module = importlib.util.module_from_spec(spec) 

60 try: 

61 spec.loader.exec_module(module) 

62 except Exception as e: 

63 _LOGGER.warning( 

64 "Failed to load runtime module %s from %s: %s", module_name, file_path, e 

65 ) 

66 return None 

67 

68 sys.modules[module_name] = module 

69 return module 

70 

71 

72# Best-effort loads at import time: succeed when the files already exist from 

73# a previous init() run, silently skipped otherwise. 

74_load_module_from_file( 

75 "ezqt_app._runtime_resources_rc", get_bin_path() / "resources_rc.py" 

76) 

77_icons_mod = _load_module_from_file( 

78 "ezqt_app._runtime_app_icons", get_bin_path() / "app_icons.py" 

79) 

80if _icons_mod is not None: 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true

81 AppIcons = getattr(_icons_mod, "AppIcons", None) 

82 

83_images_mod = _load_module_from_file( 

84 "ezqt_app._runtime_app_images", get_bin_path() / "app_images.py" 

85) 

86if _images_mod is not None: 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true

87 AppImages = getattr(_images_mod, "AppImages", None) 

88 

89 

90def load_runtime_rc() -> None: 

91 """Load (or reload) the project ``resources_rc.py`` generated by init(). 

92 

93 Also loads ``bin/app_icons.py`` and ``bin/app_images.py`` into 

94 :data:`AppIcons` and :data:`AppImages` when present. 

95 

96 Called by the bootstrap sequence after QRC compilation so that user assets 

97 are registered even when init() runs for the first time (i.e. when the 

98 files did not exist at import time). 

99 

100 Note: 

101 This function reassigns the module-level :data:`AppIcons` and 

102 :data:`AppImages` variables. Code that imported these symbols 

103 *before* ``init()`` was called holds a stale ``None`` reference. 

104 Access them via the module after ``init()`` to get the live value:: 

105 

106 # Correct — resolved at access time 

107 from ezqt_app.shared import resources 

108 icon_path = resources.AppIcons.my_icon 

109 

110 # Also correct when called after init() 

111 from ezqt_app.shared.resources import AppIcons 

112 icon_path = AppIcons.my_icon 

113 """ 

114 global AppIcons, AppImages # noqa: PLW0603 

115 

116 module_name = "ezqt_app._runtime_resources_rc" 

117 sys.modules.pop(module_name, None) 

118 _load_module_from_file(module_name, get_bin_path() / "resources_rc.py") 

119 

120 sys.modules.pop("ezqt_app._runtime_app_icons", None) 

121 icons_mod = _load_module_from_file( 

122 "ezqt_app._runtime_app_icons", get_bin_path() / "app_icons.py" 

123 ) 

124 if icons_mod is not None: 124 ↛ 127line 124 didn't jump to line 127 because the condition on line 124 was always true

125 AppIcons = getattr(icons_mod, "AppIcons", None) 

126 

127 sys.modules.pop("ezqt_app._runtime_app_images", None) 

128 images_mod = _load_module_from_file( 

129 "ezqt_app._runtime_app_images", get_bin_path() / "app_images.py" 

130 ) 

131 if images_mod is not None: 131 ↛ exitline 131 didn't return from function 'load_runtime_rc' because the condition on line 131 was always true

132 AppImages = getattr(images_mod, "AppImages", None) 

133 

134 

135# /////////////////////////////////////////////////////////////// 

136# PUBLIC API 

137# /////////////////////////////////////////////////////////////// 

138__all__ = ["Icons", "Images", "AppIcons", "AppImages", "load_runtime_rc"]