## /utils/system/properties.py
from pathlib import Path
from zoneinfo import ZoneInfo
from ..interface import ReadOnlyMeta
__all__ = [
'SystemProperties',
]
[документация]
class SystemProperties(metaclass=ReadOnlyMeta):
"""
Неизменяемая статическая структура, предоставляющая базовые свойства системы.
Не поддерживает создание экземпляров и модификацию данных.
"""
__slots__ = ()
@staticmethod
def os() -> str:
"""Возвращает имя операционной системы (например: 'windows', 'linux', 'darwin')."""
import platform
sys_type = platform.system().lower()
if sys_type == "windows":
return "windows"
if sys_type == "darwin":
return "darwin"
return "linux"
@staticmethod
def os_release() -> str:
"""Возвращает версию релиза ОС (например: '10', '11', '5.15.0-71-generic')."""
import platform
return platform.release()
@staticmethod
def theme() -> str:
"""
Кроссплатформенно определяет текущую системную тему оформления (dark/light).
"""
sys_name = SystemProperties.os
# Сценарий 1: Windows (считываем значение из системного реестра)
if sys_name == "windows":
try:
import winreg
registry_key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
)
value, _ = winreg.QueryValueEx(registry_key, "AppsUseLightTheme")
winreg.CloseKey(registry_key)
return "light" if value == 1 else "dark"
except Exception:
pass
# Сценарий 2: macOS (Darwin)
elif sys_name == "darwin":
try:
import subprocess
result = subprocess.run(
["defaults", "read", "-g", "AppleInterfaceStyle"],
capture_output=True, text=True
)
return "dark" if "Dark" in result.stdout else "light"
except Exception:
pass
import os
# Сценарий 3: Резервный вариант для Linux и контейнеров
mode = os.environ.get("GLOBAL_THEME") or os.environ.get("COLORTERM") or "light"
return "dark" if "dark" in mode.lower() else "light"
@staticmethod
def language() -> str:
"""
Возвращает точный кроссплатформенный язык интерфейса операционной системы
(например: 'ru_RU', 'en_US', 'de_DE').
"""
sys_name = SystemProperties.os
# Сценарий 1: Windows (считываем язык интерфейса ОС из системного реестра)
if sys_name == "windows":
try:
import ctypes
# Создаем буфер для хранения списка языков интерфейса пользователя (MUI)
num_languages = ctypes.c_ulong(0)
buffer_size = ctypes.c_ulong(0)
# Константа MUI_LANGUAGE_NAME (0x8) заставляет Windows вернуть имена вроде "ru-RU"
# Сначала вызываем функцию с пустым буфером, чтобы узнать требуемый размер памяти
ctypes.windll.kernel32.GetUserDefaultUILanguage() # Резервный ID на всякий случай
# Самый точный метод: GetUserPreferredUILanguages
# Он возвращает список языков интерфейса в порядке их приоритета у пользователя
if ctypes.windll.kernel32.GetUserPreferredUILanguages(8, ctypes.byref(num_languages), None, ctypes.byref(buffer_size)):
buffer = ctypes.create_unicode_buffer(buffer_size.value)
if ctypes.windll.kernel32.GetUserPreferredUILanguages(8, ctypes.byref(num_languages), buffer, ctypes.byref(buffer_size)):
# Строка возвращается в виде списка, разделенного нулевыми байтами (null-terminated).
# Берем самый первый (приоритетный) язык интерфейса в системе
raw_lang = buffer.value.split('\x00')[0]
if raw_lang:
if "-" in raw_lang:
return raw_lang.replace("-", "_")
return f"{raw_lang}_{raw_lang.upper()}"
# Резервный быстрый вариант для старых сборок Windows через LANGID (например, 1049 для RU)
lang_id = ctypes.windll.kernel32.GetUserDefaultUILanguage()
# Переводим LANGID в строковый код локали через макрос
locale_str = ctypes.create_unicode_buffer(85)
if ctypes.windll.kernel32.LCIDToLocaleName(lang_id, locale_str, 85, 0):
value = locale_str.value
if "-" in value:
return value.replace("-", "_")
return value
except Exception:
pass
# Сценарий 2: macOS / Darwin (считываем массив предпочитаемых языков пользователя)
elif sys_name == "darwin":
try:
import subprocess
result = subprocess.run(
["defaults", "read", "-g", "AppleLanguages"],
capture_output=True, text=True
)
# Вывод выглядит как: (\n "ru-RU",\n "en-US"\n)
# Извлекаем первый (самый приоритетный) язык интерфейса
for line in result.stdout.splitlines():
if '"' in line:
lang = line.split('"')[1]
if "-" in lang:
return lang.replace("-", "_")
return f"{lang}_{lang.upper()}"
except Exception:
pass
import os
# Сценарий 3: Linux и резервный вариант для всех ОС
# Перебираем стандартные переменные окружения, куда Linux/Unix пишет локаль
for env_var in ("LANG", "LC_ALL", "LC_CTYPE", "LANGUAGE"):
local_val = os.environ.get(env_var)
if local_val:
clean_val = str(local_val.split(".")[0])
if "-" in clean_val:
return clean_val.replace("-", "_")
return clean_val
return "en_US"
@staticmethod
def timezone_name() -> str:
"""Возвращает имя текущего часового пояса системы (например: 'UTC', 'MSK')."""
import time
return time.tzname[time.daylight]
@staticmethod
def timezone() -> ZoneInfo:
"""
Возвращает объект ZoneInfo текущего часового пояса системы
для удобной конвертации времени через datetime.
"""
import time
# time.tzname[0] может вернуть локальное имя, пытаемся получить IANA-идентификатор
try:
import sys
# На Unix-системах это самый надежный способ узнать IANA-имя (например, 'Europe/Moscow')
if sys.platform != "win32":
import tzlocal
return ZoneInfo(tzlocal.get_localzone_name())
except Exception:
pass
# Резервный вариант по смещению времени, если IANA имя не определилось
# (ZoneInfo умеет работать со смещениями через объекты или строки конфигурации)
offset_seconds = -time.timezone if not time.daylight else -time.altzone
return ZoneInfo(f"Etc/GMT{-offset_seconds // 3600:+d}".replace("+-", "-").replace("++", "+"))
@staticmethod
def date_format() -> str:
"""Возвращает стандартный формат даты, принятый в текущей локали."""
import sys
import locale
# Для России это обычно '%d.%m.%Y', для США '%m/%d/%Y'
# Извлекаем базовый строковый маркер даты из настроек локали
if sys.platform != "win32":
try:
return locale.nl_langinfo(locale.D_FMT) # type: ignore[attr-defined] # Работает на Unix-системах
except (AttributeError, ValueError):
pass
return "%d.%m.%Y" if "ru" in SystemProperties.language.lower() else "%m/%d/%Y" # type: ignore
@staticmethod
def screen_width() -> int:
"""Возвращает точную физическую ширину главного (первичного) монитора в системе."""
sys_name = SystemProperties.os
# --- 1. WINDOWS USER32 API ---
if sys_name == "windows":
try:
import ctypes
# 0 = SM_CXSCREEN (Ширина первичного монитора)
return int(ctypes.windll.user32.GetSystemMetrics(0))
except Exception:
return 1920
# --- 2. MACOS (DARWIN) CORE GRAPHICS API ---
elif sys_name == "darwin":
try:
import ctypes
import ctypes.util
# Загружаем ApplicationServices, содержащий CoreGraphics API
lib_path = ctypes.util.find_library('ApplicationServices')
if lib_path:
core_graphics = ctypes.CDLL(lib_path)
# CGMainDisplayID() возвращает ID главного дисплея (обычно 1)
main_display = core_graphics.CGMainDisplayID()
# CGDisplayPixelsWide возвращает точную физическую ширину экрана
width = core_graphics.CGDisplayPixelsWide(main_display)
if width > 0:
return int(width)
except Exception:
pass
# Резервный консольный парсер для Mac через системный профайлер Displays
try:
import subprocess
import re
cmd = "system_profiler SPDisplaysDataType | grep Resolution"
output = subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.DEVNULL)
match = re.search(r"(\d+)\s*x\s*(\d+)", output)
if match:
return int(match.group(1))
except Exception:
pass
return 1440 # Безопасный кроссплатформенный Retina-откат
# --- 3. LINUX (X11 / WAYLAND XRANDR ENGINE) ---
else:
try:
# Сканируем утилиту xrandr на наличие флага 'primary'
import subprocess
import re
output = subprocess.check_output(["xrandr", "--current"], text=True, stderr=subprocess.DEVNULL)
for line in output.splitlines():
if "primary" in line:
match = re.search(r"(\d+)x(\d+)\+", line)
if match:
return int(match.group(1))
except Exception:
try:
# Резервный сборщик для старых версий графических серверов X11
import subprocess
import re
output = subprocess.check_output(["xdpyinfo"], text=True, stderr=subprocess.DEVNULL)
for line in output.splitlines():
if "dimensions:" in line:
match = re.search(r"(\d+)x(\d+)\s+pixels", line)
if match:
return int(match.group(1))
except Exception:
pass
return 1920
@staticmethod
def screen_height() -> int:
"""Возвращает точную физическую высоту главного (первичного) монитора в системе."""
sys_name = SystemProperties.os
# --- 1. WINDOWS USER32 API ---
if sys_name == "windows":
try:
import ctypes
# 1 = SM_CYSCREEN (Высота первичного монитора)
return int(ctypes.windll.user32.GetSystemMetrics(1))
except Exception:
return 1080
# --- 2. MACOS (DARWIN) CORE GRAPHICS API ---
elif sys_name == "darwin":
try:
import ctypes
import ctypes.util
lib_path = ctypes.util.find_library('ApplicationServices')
if lib_path:
core_graphics = ctypes.CDLL(lib_path)
main_display = core_graphics.CGMainDisplayID()
height = core_graphics.CGDisplayPixelsHigh(main_display)
if height > 0:
return int(height)
except Exception:
pass
try:
import subprocess
import re
cmd = "system_profiler SPDisplaysDataType | grep Resolution"
output = subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.DEVNULL)
match = re.search(r"(\d+)\s*x\s*(\d+)", output)
if match:
return int(match.group(2))
except Exception:
pass
return 900
# --- 3. LINUX (X11 / WAYLAND XRANDR ENGINE) ---
else:
try:
import subprocess
import re
output = subprocess.check_output(["xrandr", "--current"], text=True, stderr=subprocess.DEVNULL)
for line in output.splitlines():
if "primary" in line:
match = re.search(r"(\d+)x(\d+)\+", line)
if match:
return int(match.group(2))
except Exception:
try:
import subprocess
import re
output = subprocess.check_output(["xdpyinfo"], text=True, stderr=subprocess.DEVNULL)
for line in output.splitlines():
if "dimensions:" in line:
match = re.search(r"(\d+)x(\d+)\s+pixels", line)
if match:
return int(match.group(2))
except Exception:
pass
return 1080
@staticmethod
def is_64bit() -> bool:
"""Определяет, является ли архитектура процессора 64-битной."""
import sys
return sys.maxsize > 2**32
@staticmethod
def python_version() -> str:
"""Возвращает текущую версию интерпретатора Python."""
import sys
return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
@staticmethod
def cpu_count() -> int:
"""Возвращает количество логических ядер процессора в системе. Если не определено, вернет 1."""
import os
return os.cpu_count() or 1
@staticmethod
def user_home() -> Path:
"""Возвращает объект Path, указывающий на домашнюю директорию текущего пользователя."""
return Path.home()
@staticmethod
def user_name() -> str:
"""Возвращает имя текущего пользователя в операционной системе."""
import sys
import os
# Кроссплатформенный способ получения имени пользователя
return os.getlogin() if sys.platform == "win32" else os.environ.get("USER", os.environ.get("LOGNAME", "unknown"))
[документация]
@classmethod
def clear(cls) -> None:
"""Реализация контракта Disposable.."""
pass
[документация]
@classmethod
def destroy(cls) -> None:
"""Реализация контракта Disposable."""
pass