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
« 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# ///////////////////////////////////////////////////////////////
6"""System-level startup configuration — encoding, locale, env vars, project root."""
8from __future__ import annotations
10# ///////////////////////////////////////////////////////////////
11# IMPORTS
12# ///////////////////////////////////////////////////////////////
13# Standard library imports
14import contextlib
15import locale
16import os
17import sys
18from pathlib import Path
21# ///////////////////////////////////////////////////////////////
22# CLASSES
23# ///////////////////////////////////////////////////////////////
24class StartupConfig:
25 """Manages system-level startup configuration.
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 """
32 def __init__(self) -> None:
33 self._configured = False
35 def configure(self, project_root: Path | None = None) -> None:
36 """Run all startup configuration steps (idempotent)."""
37 if self._configured:
38 return
40 self._configure_encoding()
41 self._configure_environment()
42 self._configure_locale()
43 self._configure_system()
44 self._configure_project_root(project_root)
46 self._configured = True
48 # ------------------------------------------------------------------
49 # Configuration steps
50 # ------------------------------------------------------------------
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]
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"
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, "")
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()
80 def _configure_windows(self) -> None:
81 os.environ["QT_QPA_PLATFORM"] = "windows:dpiawareness=0"
83 def _configure_linux(self) -> None:
84 os.environ["QT_QPA_PLATFORM"] = "xcb"
86 def _configure_macos(self) -> None:
87 os.environ["QT_QPA_PLATFORM"] = "cocoa"
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
93 detected_root = project_root or Path.cwd()
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
109 AppService.set_project_root(detected_root)
111 # ------------------------------------------------------------------
112 # Accessors
113 # ------------------------------------------------------------------
115 def get_encoding(self) -> str:
116 return getattr(sys.stdout, "encoding", "utf-8")
118 def get_locale(self) -> str | None:
119 try:
120 return locale.getlocale()[0]
121 except (locale.Error, IndexError):
122 return None
124 def is_configured(self) -> bool:
125 return self._configured
127 def reset(self) -> None:
128 self._configured = False