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

49 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 07:07 +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 logging 

15import sys 

16from pathlib import Path 

17from types import ModuleType 

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

19 

20# Local imports 

21from ...resources import base_resources_rc # type: ignore # noqa: F401 

22from ...utils.runtime_paths import get_bin_path 

23from .icons import Icons as _PackageIcons 

24from .images import Images as _PackageImages 

25 

26Icons = _PackageIcons 

27Images = _PackageImages 

28 

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

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

31# 

32# WARNING — import pattern: 

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

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

35# 

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

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

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

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

40AppIcons: Any = None 

41AppImages: Any = None 

42 

43 

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

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

46 if not file_path.exists() or not file_path.is_file(): 46 ↛ 49line 46 didn't jump to line 49 because the condition on line 46 was always true

47 return None 

48 

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

50 if spec is None or spec.loader is None: 

51 return None 

52 

53 module = importlib.util.module_from_spec(spec) 

54 try: 

55 spec.loader.exec_module(module) 

56 except Exception as e: 

57 logging.getLogger(__name__).warning( 

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

59 ) 

60 return None 

61 

62 sys.modules[module_name] = module 

63 return module 

64 

65 

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

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

68_load_module_from_file( 

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

70) 

71_icons_mod = _load_module_from_file( 

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

73) 

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

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

76 

77_images_mod = _load_module_from_file( 

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

79) 

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

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

82 

83 

84def load_runtime_rc() -> None: 

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

86 

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

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

89 

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

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

92 files did not exist at import time). 

93 

94 Note: 

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

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

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

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

99 

100 # Correct — resolved at access time 

101 from ezqt_app.shared import resources 

102 icon_path = resources.AppIcons.my_icon 

103 

104 # Also correct when called after init() 

105 from ezqt_app.shared.resources import AppIcons 

106 icon_path = AppIcons.my_icon 

107 """ 

108 global AppIcons, AppImages # noqa: PLW0603 

109 

110 module_name = "ezqt_app._runtime_resources_rc" 

111 sys.modules.pop(module_name, None) 

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

113 

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

115 icons_mod = _load_module_from_file( 

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

117 ) 

118 if icons_mod is not None: 

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

120 

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

122 images_mod = _load_module_from_file( 

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

124 ) 

125 if images_mod is not None: 

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

127 

128 

129# /////////////////////////////////////////////////////////////// 

130# PUBLIC API 

131# /////////////////////////////////////////////////////////////// 

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