Source code for gpype.common.settings

import os
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any, Optional
from xml.dom import minidom


[docs] class _Settings(dict): """Singleton settings manager for g.Pype application configuration. Manages persistent settings stored in XML format in platform-specific directories. Provides automatic type conversion, default values, and thread-safe singleton access. Storage locations: - Windows: %PROGRAMDATA%/gtec/gPype/settings.xml - macOS: ~/Library/Application Support/gtec/gPype/settings.xml Use Settings.get() to access the singleton instance. """ #: Default settings applied on first initialization DEFAULTS = {"Key": ""} #: Singleton instance reference _instance: Optional["_Settings"] = None
[docs] def __init__(self): """Initialize the settings singleton instance. Loads existing settings from XML file, applies default values for missing keys, and saves the updated configuration. Raises: RuntimeError: If an instance already exists. Use get() instead. """ # Enforce singleton pattern if _Settings._instance is not None: raise RuntimeError( "Use Settings.get_instance() to access the " "singleton instance." ) # Initialize dictionary base class super().__init__() # Set up file path and ensure directory exists self.file_path = self._get_settings_path() self._ensure_path_exists() # Load existing settings from file loaded_settings = self._read() self.update(loaded_settings) # Apply default values for missing keys updated = False for key, value in _Settings.DEFAULTS.items(): if key not in self: self[key] = value updated = True # Save updated settings if defaults were added if updated: self.write() # Register as singleton instance _Settings._instance = self
[docs] @staticmethod def get() -> "_Settings": """Get the singleton settings instance. Creates the instance if it doesn't exist, otherwise returns the existing instance. Returns: _Settings: The singleton settings instance. """ if _Settings._instance is None: _Settings() return _Settings._instance
def _get_settings_path(self) -> Path: """Determine platform-specific settings file path. Returns: Path: Full path to the settings XML file. Raises: RuntimeError: If the operating system is not supported. """ # Check for environment variable override (useful for testing) if "GPYPE_SETTINGS_DIR" in os.environ: base = Path(os.environ["GPYPE_SETTINGS_DIR"]) return base / "settings.xml" if sys.platform == "win32": # Windows: Use PROGRAMDATA for system-wide settings base = Path(os.getenv("PROGRAMDATA", r"C:\ProgramData")) elif sys.platform == "darwin": # macOS: Use user's Application Support directory base = Path.home() / "Library" / "Application Support" else: # Unsupported platform raise RuntimeError("Unsupported OS") return base / "gtec" / "gPype" / "settings.xml" def _ensure_path_exists(self): """Create the settings directory if it doesn't exist.""" self.file_path.parent.mkdir(parents=True, exist_ok=True) def _convert_type(self, value: str) -> Any: """Convert string values to appropriate Python types. Args: value (str): String value to convert. Returns: Any: Converted value (bool, int, float, or str). """ val = value.strip().lower() # Convert boolean values if val == "true": return True if val == "false": return False # Try integer conversion try: return int(value) except ValueError: pass # Try float conversion try: return float(value) except ValueError: pass # Default to string return value def _read(self) -> dict[str, Any]: """Read settings from the XML file. Returns: dict[str, Any]: Dictionary of setting key-value pairs. """ # Return empty dict if file doesn't exist if not self.file_path.exists(): return {} try: # Parse XML file tree = ET.parse(self.file_path) root = tree.getroot() # Convert each XML element to key-value pair with type conversion return { child.tag: self._convert_type(child.text or "") for child in root } except Exception as e: # Log warning but continue with empty settings print(f"Warning: Failed to parse settings file: {e}") return {}
[docs] def write(self): """Write current settings to the XML file with pretty formatting.""" # Create XML root element root = ET.Element("Settings") # Add each setting as a child element for key, value in self.items(): elem = ET.SubElement(root, key) elem.text = str(value) # Convert to pretty-formatted XML string rough_string = ET.tostring(root, "utf-8") pretty_string = minidom.parseString(rough_string).toprettyxml( indent=" " ) # Write to file with UTF-8 encoding with open(self.file_path, "w", encoding="utf-8") as f: f.write(pretty_string)
#: Global settings instance for convenient access throughout the application Settings: _Settings = _Settings()