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

70 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-06 13:12 +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 

19from typing import Any, cast 

20 

21 

22# /////////////////////////////////////////////////////////////// 

23# CLASSES 

24# /////////////////////////////////////////////////////////////// 

25class StartupConfig: 

26 """Manages system-level startup configuration. 

27 

28 Handles encoding, locale, environment variables and project root 

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

30 no-ops once the instance is configured. 

31 """ 

32 

33 def __init__(self) -> None: 

34 self._configured = False 

35 

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

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

38 if self._configured: 

39 return 

40 

41 self._configure_encoding() 

42 self._configure_environment() 

43 self._configure_locale() 

44 self._configure_system() 

45 self._configure_project_root(project_root) 

46 

47 self._configured = True 

48 

49 # ------------------------------------------------------------------ 

50 # Configuration steps 

51 # ------------------------------------------------------------------ 

52 

53 def _configure_encoding(self) -> None: 

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

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

56 cast(Any, sys.stdout).reconfigure(encoding="utf-8") 

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

58 cast(Any, sys.stderr).reconfigure(encoding="utf-8") 

59 

60 def _configure_environment(self) -> None: 

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

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

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

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

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

66 

67 def _configure_locale(self) -> None: 

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

69 with contextlib.suppress(locale.Error): 

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

71 

72 def _configure_system(self) -> None: 

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

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

75 self._configure_windows() 

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

77 self._configure_linux() 

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

79 self._configure_macos() 

80 

81 def _configure_windows(self) -> None: 

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

83 

84 def _configure_linux(self) -> None: 

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

86 

87 def _configure_macos(self) -> None: 

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

89 

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

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

92 from ..application.app_service import AppService 

93 

94 detected_root = project_root or Path.cwd() 

95 

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

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

98 detected_root = detected_root.parent 

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

100 pass # Already at project root 

101 else: 

102 # Walk up looking for a main.py 

103 current = detected_root 

104 while current.parent != current: 

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

106 detected_root = current.parent 

107 break 

108 current = current.parent 

109 

110 AppService.set_project_root(detected_root) 

111 

112 # ------------------------------------------------------------------ 

113 # Accessors 

114 # ------------------------------------------------------------------ 

115 

116 def get_encoding(self) -> str: 

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

118 

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

120 try: 

121 return locale.getlocale()[0] 

122 except (locale.Error, IndexError): 

123 return None 

124 

125 def is_configured(self) -> bool: 

126 return self._configured 

127 

128 def reset(self) -> None: 

129 self._configured = False