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
« 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# ///////////////////////////////////////////////////////////////
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
19from typing import Any, cast
22# ///////////////////////////////////////////////////////////////
23# CLASSES
24# ///////////////////////////////////////////////////////////////
25class StartupConfig:
26 """Manages system-level startup configuration.
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 """
33 def __init__(self) -> None:
34 self._configured = False
36 def configure(self, project_root: Path | None = None) -> None:
37 """Run all startup configuration steps (idempotent)."""
38 if self._configured:
39 return
41 self._configure_encoding()
42 self._configure_environment()
43 self._configure_locale()
44 self._configure_system()
45 self._configure_project_root(project_root)
47 self._configured = True
49 # ------------------------------------------------------------------
50 # Configuration steps
51 # ------------------------------------------------------------------
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")
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"
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, "")
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()
81 def _configure_windows(self) -> None:
82 os.environ["QT_QPA_PLATFORM"] = "windows:dpiawareness=0"
84 def _configure_linux(self) -> None:
85 os.environ["QT_QPA_PLATFORM"] = "xcb"
87 def _configure_macos(self) -> None:
88 os.environ["QT_QPA_PLATFORM"] = "cocoa"
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
94 detected_root = project_root or Path.cwd()
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
110 AppService.set_project_root(detected_root)
112 # ------------------------------------------------------------------
113 # Accessors
114 # ------------------------------------------------------------------
116 def get_encoding(self) -> str:
117 return getattr(sys.stdout, "encoding", "utf-8")
119 def get_locale(self) -> str | None:
120 try:
121 return locale.getlocale()[0]
122 except (locale.Error, IndexError):
123 return None
125 def is_configured(self) -> bool:
126 return self._configured
128 def reset(self) -> None:
129 self._configured = False