Coverage for src / ezqt_app / services / bootstrap / startup_config.py: 70.79%

69 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 07:07 +0000

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

2# SERVICES.BOOTSTRAP.STARTUP_CONFIG - System startup configuration 

3# Project: ezqt_app 

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

5 

6"""System-level startup configuration — encoding, locale, env vars, project root.""" 

7 

8from __future__ import annotations 

9 

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

11# IMPORTS 

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

13# Standard library imports 

14import contextlib 

15import locale 

16import os 

17import sys 

18from pathlib import Path 

19 

20 

21# /////////////////////////////////////////////////////////////// 

22# CLASSES 

23# /////////////////////////////////////////////////////////////// 

24class StartupConfig: 

25 """Manages system-level startup configuration. 

26 

27 Handles encoding, locale, environment variables and project root 

28 detection. Idempotent: subsequent calls to :meth:`configure` are 

29 no-ops once the instance is configured. 

30 """ 

31 

32 def __init__(self) -> None: 

33 self._configured = False 

34 

35 def configure(self, project_root: Path | None = None) -> None: 

36 """Run all startup configuration steps (idempotent).""" 

37 if self._configured: 

38 return 

39 

40 self._configure_encoding() 

41 self._configure_environment() 

42 self._configure_locale() 

43 self._configure_system() 

44 self._configure_project_root(project_root) 

45 

46 self._configured = True 

47 

48 # ------------------------------------------------------------------ 

49 # Configuration steps 

50 # ------------------------------------------------------------------ 

51 

52 def _configure_encoding(self) -> None: 

53 """Force UTF-8 encoding on stdout/stderr.""" 

54 if hasattr(sys.stdout, "reconfigure"): 54 ↛ 56line 54 didn't jump to line 56 because the condition on line 54 was always true

55 sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr] 

56 if hasattr(sys.stderr, "reconfigure"): 56 ↛ exitline 56 didn't return from function '_configure_encoding' because the condition on line 56 was always true

57 sys.stderr.reconfigure(encoding="utf-8") # type: ignore[union-attr] 

58 

59 def _configure_environment(self) -> None: 

60 """Set mandatory Qt/Python environment variables.""" 

61 os.environ["PYTHONIOENCODING"] = "utf-8" 

62 os.environ["QT_FONT_DPI"] = "96" 

63 os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" 

64 os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "PassThrough" 

65 

66 def _configure_locale(self) -> None: 

67 """Set locale to the system default (UTF-8 preferred).""" 

68 with contextlib.suppress(locale.Error): 

69 locale.setlocale(locale.LC_ALL, "") 

70 

71 def _configure_system(self) -> None: 

72 """Apply platform-specific Qt environment variables.""" 

73 if sys.platform.startswith("win"): 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true

74 self._configure_windows() 

75 elif sys.platform.startswith("linux"): 75 ↛ 77line 75 didn't jump to line 77 because the condition on line 75 was always true

76 self._configure_linux() 

77 elif sys.platform.startswith("darwin"): 

78 self._configure_macos() 

79 

80 def _configure_windows(self) -> None: 

81 os.environ["QT_QPA_PLATFORM"] = "windows:dpiawareness=0" 

82 

83 def _configure_linux(self) -> None: 

84 os.environ["QT_QPA_PLATFORM"] = "xcb" 

85 

86 def _configure_macos(self) -> None: 

87 os.environ["QT_QPA_PLATFORM"] = "cocoa" 

88 

89 def _configure_project_root(self, project_root: Path | None = None) -> None: 

90 """Detect and register the project root with the config service.""" 

91 from ..application.app_service import AppService 

92 

93 detected_root = project_root or Path.cwd() 

94 

95 # If running from bin/, go up one level 

96 if detected_root.name == "bin" and (detected_root.parent / "main.py").exists(): 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 detected_root = detected_root.parent 

98 elif (detected_root / "main.py").exists(): 98 ↛ 102line 98 didn't jump to line 102 because the condition on line 98 was always true

99 pass # Already at project root 

100 else: 

101 # Walk up looking for a main.py 

102 current = detected_root 

103 while current.parent != current: 

104 if (current.parent / "main.py").exists(): 

105 detected_root = current.parent 

106 break 

107 current = current.parent 

108 

109 AppService.set_project_root(detected_root) 

110 

111 # ------------------------------------------------------------------ 

112 # Accessors 

113 # ------------------------------------------------------------------ 

114 

115 def get_encoding(self) -> str: 

116 return getattr(sys.stdout, "encoding", "utf-8") 

117 

118 def get_locale(self) -> str | None: 

119 try: 

120 return locale.getlocale()[0] 

121 except (locale.Error, IndexError): 

122 return None 

123 

124 def is_configured(self) -> bool: 

125 return self._configured 

126 

127 def reset(self) -> None: 

128 self._configured = False