From 239c5b6e249f260ecbff413560cc3b94c855f4c2 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Thu, 19 Oct 2023 19:24:43 -0400 Subject: [PATCH] [2051] Config Files --- config/__init__.py | 72 +++++++++++------------- config/darwin.py | 24 ++++---- config/linux.py | 136 ++++++++++++++++++++++----------------------- config/windows.py | 98 +++++++++++++++----------------- 4 files changed, 156 insertions(+), 174 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index a31cdd89..ae08f3a0 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,12 +1,14 @@ """ -Code dealing with the configuration of the program. +__init__.py - Code dealing with the configuration of the program. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. Windows uses the Registry to store values in a flat manner. Linux uses a file, but for commonality it's still a flat data structure. macOS uses a 'defaults' object. """ - - __all__ = [ # defined in the order they appear in the file 'GITVERSION_FILE', @@ -40,10 +42,8 @@ import sys import traceback import warnings from abc import abstractmethod -from typing import Any, Callable, Optional, Type, TypeVar - +from typing import Any, Callable, Optional, Type, TypeVar, Union, List import semantic_version - from constants import GITVERSION_FILE, applongname, appname # Any of these may be imported by plugins @@ -52,7 +52,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.9.5' +_static_appversion = '5.10.0-alpha0' _cached_version: Optional[semantic_version.Version] = None copyright = '© 2015-2019 Jonathan Harris, 2020-2023 EDCD' @@ -60,10 +60,10 @@ copyright = '© 2015-2019 Jonathan Harris, 2020-2023 EDCD' update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' update_interval = 8*60*60 # Providers marked to be in debug mode. Generally this is expected to switch to sending data to a log file -debug_senders: list[str] = [] +debug_senders: List[str] = [] # TRACE logging code that should actually be used. Means not spamming it # *all* if only interested in some things. -trace_on: list[str] = [] +trace_on: List[str] = [] capi_pretend_down: bool = False capi_debug_access_token: Optional[str] = None @@ -79,7 +79,6 @@ else: _T = TypeVar('_T') -########################################################################### def git_shorthash_from_head() -> str: """ Determine short hash for current git HEAD. @@ -91,13 +90,14 @@ def git_shorthash_from_head() -> str: shorthash: str = None # type: ignore try: - git_cmd = subprocess.Popen('git rev-parse --short HEAD'.split(), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) + git_cmd = subprocess.Popen( + "git rev-parse --short HEAD".split(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) out, err = git_cmd.communicate() - except Exception as e: + except subprocess.CalledProcessError as e: logger.info(f"Couldn't run git command for short hash: {e!r}") else: @@ -131,7 +131,7 @@ def appversion() -> semantic_version.Version: if getattr(sys, 'frozen', False): # Running frozen, so we should have a .gitversion file # Yes, .parent because if frozen we're inside library.zip - with open(pathlib.Path(sys.path[0]).parent / GITVERSION_FILE, 'r', encoding='utf-8') as gitv: + with open(pathlib.Path(sys.path[0]).parent / GITVERSION_FILE, encoding='utf-8') as gitv: shorthash = gitv.read() else: @@ -157,23 +157,15 @@ def appversion_nobuild() -> semantic_version.Version: :return: App version without any build meta data. """ return appversion().truncate('prerelease') -########################################################################### class AbstractConfig(abc.ABC): """Abstract root class of all platform specific Config implementations.""" OUT_EDDN_SEND_STATION_DATA = 1 - # OUT_MKT_BPC = 2 # No longer supported OUT_MKT_TD = 4 OUT_MKT_CSV = 8 OUT_SHIP = 16 - # OUT_SHIP_EDS = 16 # Replaced by OUT_SHIP - # OUT_SYS_FILE = 32 # No longer supported - # OUT_STAT = 64 # No longer available - # OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP - # OUT_SYS_EDSM = 256 # Now a plugin - # OUT_SYS_AUTO = 512 # Now always automatic OUT_MKT_MANUAL = 1024 OUT_EDDN_SEND_NON_STATION = 2048 OUT_EDDN_DELAY = 4096 @@ -185,7 +177,6 @@ class AbstractConfig(abc.ABC): respath_path: pathlib.Path home_path: pathlib.Path default_journal_dir_path: pathlib.Path - identifier: str __in_shutdown = False # Is the application currently shutting down ? @@ -294,7 +285,7 @@ class AbstractConfig(abc.ABC): @staticmethod def _suppress_call( - func: Callable[..., _T], exceptions: Type[BaseException] | list[Type[BaseException]] = Exception, + func: Callable[..., _T], exceptions: Union[Type[BaseException], List[Type[BaseException]]] = Exception, *args: Any, **kwargs: Any ) -> Optional[_T]: if exceptions is None: @@ -303,15 +294,15 @@ class AbstractConfig(abc.ABC): if not isinstance(exceptions, list): exceptions = [exceptions] - with contextlib.suppress(*exceptions): # type: ignore # it works fine, mypy + with contextlib.suppress(*exceptions): return func(*args, **kwargs) return None def get( self, key: str, - default: list | str | bool | int | None = None - ) -> list | str | bool | int | None: + default: Union[list, str, bool, int, None] = None + ) -> Union[list, str, bool, int, None]: """ Return the data for the requested key, or a default. @@ -326,19 +317,19 @@ class AbstractConfig(abc.ABC): if (a_list := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None: return a_list - elif (a_str := self._suppress_call(self.get_str, ValueError, key, default=None)) is not None: + if (a_str := self._suppress_call(self.get_str, ValueError, key, default=None)) is not None: return a_str - elif (a_bool := self._suppress_call(self.get_bool, ValueError, key, default=None)) is not None: + if (a_bool := self._suppress_call(self.get_bool, ValueError, key, default=None)) is not None: return a_bool - elif (an_int := self._suppress_call(self.get_int, ValueError, key, default=None)) is not None: + if (an_int := self._suppress_call(self.get_int, ValueError, key, default=None)) is not None: return an_int - return default # type: ignore + return default @abstractmethod - def get_list(self, key: str, *, default: list | None = None) -> list: + def get_list(self, key: str, *, default: Optional[list] = None) -> list: """ Return the list referred to by the given key if it exists, or the default. @@ -347,7 +338,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def get_str(self, key: str, *, default: str | None = None) -> str: + def get_str(self, key: str, *, default: Optional[str] = None) -> str: """ Return the string referred to by the given key if it exists, or the default. @@ -360,7 +351,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def get_bool(self, key: str, *, default: bool | None = None) -> bool: + def get_bool(self, key: str, *, default: Optional[bool] = None) -> bool: """ Return the bool referred to by the given key if it exists, or the default. @@ -400,7 +391,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def set(self, key: str, val: int | str | list[str] | bool) -> None: + def set(self, key: str, val: Union[int, str, List[str], bool]) -> None: """ Set the given key's data to the given value. @@ -462,16 +453,15 @@ def get_config(*args, **kwargs) -> AbstractConfig: from .darwin import MacConfig return MacConfig(*args, **kwargs) - elif sys.platform == "win32": # pragma: sys-platform-win32 + if sys.platform == "win32": # pragma: sys-platform-win32 from .windows import WinConfig return WinConfig(*args, **kwargs) - elif sys.platform == "linux": # pragma: sys-platform-linux + if sys.platform == "linux": # pragma: sys-platform-linux from .linux import LinuxConfig return LinuxConfig(*args, **kwargs) - else: # pragma: sys-platform-not-known - raise ValueError(f'Unknown platform: {sys.platform=}') + raise ValueError(f'Unknown platform: {sys.platform=}') config = get_config() diff --git a/config/darwin.py b/config/darwin.py index 895218a8..68d30306 100644 --- a/config/darwin.py +++ b/config/darwin.py @@ -1,13 +1,17 @@ -"""Darwin/macOS implementation of AbstractConfig.""" +""" +darwin.py - Darwin/macOS implementation of AbstractConfig. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" import pathlib import sys from typing import Any, Dict, List, Union - from Foundation import ( # type: ignore NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults, NSUserDomainMask ) - from config import AbstractConfig, appname, logger assert sys.platform == 'darwin' @@ -82,7 +86,7 @@ class MacConfig(AbstractConfig): """ res = self.__raw_get(key) if res is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default # Yes it could be None, but we're _assuming_ that people gave us a default if not isinstance(res, str): raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}') @@ -97,9 +101,9 @@ class MacConfig(AbstractConfig): """ res = self.__raw_get(key) if res is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default # Yes it could be None, but we're _assuming_ that people gave us a default - elif not isinstance(res, list): + if not isinstance(res, list): raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}') return res @@ -114,7 +118,7 @@ class MacConfig(AbstractConfig): if res is None: return default - elif not isinstance(res, (str, int)): + if not isinstance(res, (str, int)): raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}') try: @@ -122,7 +126,7 @@ class MacConfig(AbstractConfig): except ValueError as e: logger.error(f'__raw_get returned {res!r} which cannot be parsed to an int: {e}') - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default # Yes it could be None, but we're _assuming_ that people gave us a default def get_bool(self, key: str, *, default: bool = None) -> bool: """ @@ -132,9 +136,9 @@ class MacConfig(AbstractConfig): """ res = self.__raw_get(key) if res is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default # Yes it could be None, but we're _assuming_ that people gave us a default - elif not isinstance(res, bool): + if not isinstance(res, bool): raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}') return res diff --git a/config/linux.py b/config/linux.py index 5d543d3f..07876481 100644 --- a/config/linux.py +++ b/config/linux.py @@ -1,9 +1,15 @@ -"""Linux config implementation.""" +""" +linux.py - Linux config implementation. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" import os import pathlib import sys from configparser import ConfigParser - +from typing import Optional, Union, List from config import AbstractConfig, appname, logger assert sys.platform == 'linux' @@ -13,100 +19,97 @@ class LinuxConfig(AbstractConfig): """Linux implementation of AbstractConfig.""" SECTION = 'config' - # TODO: I dislike this, would rather use a sane config file format. But here we are. + __unescape_lut = {'\\': '\\', 'n': '\n', ';': ';', 'r': '\r', '#': '#'} __escape_lut = {'\\': '\\', '\n': 'n', ';': ';', '\r': 'r'} - def __init__(self, filename: str | None = None) -> None: + def __init__(self, filename: Optional[str] = None) -> None: + """ + Initialize LinuxConfig instance. + + :param filename: Optional file name to use for configuration storage. + """ super().__init__() - # http://standards.freedesktop.org/basedir-spec/latest/ar01s03.html + + # Initialize directory paths xdg_data_home = pathlib.Path(os.getenv('XDG_DATA_HOME', default='~/.local/share')).expanduser() self.app_dir_path = xdg_data_home / appname self.app_dir_path.mkdir(exist_ok=True, parents=True) self.plugin_dir_path = self.app_dir_path / 'plugins' self.plugin_dir_path.mkdir(exist_ok=True) - self.respath_path = pathlib.Path(__file__).parent.parent - self.internal_plugin_dir_path = self.respath_path / 'plugins' self.default_journal_dir_path = None # type: ignore - self.identifier = f'uk.org.marginal.{appname.lower()}' # TODO: Unused? + # Configure the filename config_home = pathlib.Path(os.getenv('XDG_CONFIG_HOME', default='~/.config')).expanduser() - - self.filename = config_home / appname / f'{appname}.ini' - if filename is not None: - self.filename = pathlib.Path(filename) - + self.filename = pathlib.Path(filename) if filename is not None else config_home / appname / f'{appname}.ini' self.filename.parent.mkdir(exist_ok=True, parents=True) - self.config: ConfigParser | None = ConfigParser(comment_prefixes=('#',), interpolation=None) - self.config.read(self.filename) # read() ignores files that dont exist + # Initialize the configuration + self.config = ConfigParser(comment_prefixes=('#',), interpolation=None) + self.config.read(self.filename) # Ensure that our section exists. This is here because configparser will happily create files for us, but it # does not magically create sections try: - self.config[self.SECTION].get("this_does_not_exist", fallback=None) + self.config[self.SECTION].get("this_does_not_exist") except KeyError: - logger.info("Config section not found. Backing up existing file (if any) and readding a section header") + logger.info("Config section not found. Backing up existing file (if any) and re-adding a section header") if self.filename.exists(): - (self.filename.parent / f'{appname}.ini.backup').write_bytes(self.filename.read_bytes()) - + backup_filename = self.filename.parent / f'{appname}.ini.backup' + backup_filename.write_bytes(self.filename.read_bytes()) self.config.add_section(self.SECTION) - if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir(): + # Set 'outdir' if not specified or invalid + outdir = self.get_str('outdir') + if outdir is None or not pathlib.Path(outdir).is_dir(): self.set('outdir', self.home) def __escape(self, s: str) -> str: """ - Escape a string using self.__escape_lut. + Escape special characters in a string. - This does NOT support multi-character escapes. - - :param s: str - String to be escaped. - :return: str - The escaped string. + :param s: The input string. + :return: The escaped string. """ - out = "" + escaped_chars = [] + for c in s: - if c not in self.__escape_lut: - out += c - continue + escaped_chars.append(self.__escape_lut.get(c, c)) - out += '\\' + self.__escape_lut[c] - - return out + return ''.join(escaped_chars) def __unescape(self, s: str) -> str: """ - Unescape a string. + Unescape special characters in a string. - :param s: str - The string to unescape. - :return: str - The unescaped string. + :param s: The input string. + :return: The unescaped string. """ - out: list[str] = [] + unescaped_chars = [] i = 0 while i < len(s): - c = s[i] - if c != '\\': - out.append(c) + current_char = s[i] + if current_char != '\\': + unescaped_chars.append(current_char) i += 1 continue - # We have a backslash, check what its escaping - if i == len(s)-1: + if i == len(s) - 1: raise ValueError('Escaped string has unescaped trailer') - unescaped = self.__unescape_lut.get(s[i+1]) + unescaped = self.__unescape_lut.get(s[i + 1]) if unescaped is None: - raise ValueError(f'Unknown escape: \\ {s[i+1]}') + raise ValueError(f'Unknown escape: \\{s[i + 1]}') - out.append(unescaped) + unescaped_chars.append(unescaped) i += 2 - return "".join(out) + return "".join(unescaped_chars) - def __raw_get(self, key: str) -> str | None: + def __raw_get(self, key: str) -> Optional[str]: """ Get a raw data value from the config file. @@ -118,7 +121,7 @@ class LinuxConfig(AbstractConfig): return self.config[self.SECTION].get(key) - def get_str(self, key: str, *, default: str | None = None) -> str: + def get_str(self, key: str, *, default: Optional[str] = None) -> str: """ Return the string referred to by the given key if it exists, or the default. @@ -126,29 +129,28 @@ class LinuxConfig(AbstractConfig): """ data = self.__raw_get(key) if data is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default or "" if '\n' in data: - raise ValueError('asked for string, got list') + raise ValueError('Expected string, but got list') return self.__unescape(data) - def get_list(self, key: str, *, default: list | None = None) -> list: + def get_list(self, key: str, *, default: Optional[list] = None) -> list: """ Return the list referred to by the given key if it exists, or the default. Implements :meth:`AbstractConfig.get_list`. """ data = self.__raw_get(key) - if data is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default or [] split = data.split('\n') if split[-1] != ';': raise ValueError('Encoded list does not have trailer sentinel') - return list(map(self.__unescape, split[:-1])) + return [self.__unescape(item) for item in split[:-1]] def get_int(self, key: str, *, default: int = 0) -> int: """ @@ -157,55 +159,47 @@ class LinuxConfig(AbstractConfig): Implements :meth:`AbstractConfig.get_int`. """ data = self.__raw_get(key) - if data is None: return default try: return int(data) - except ValueError as e: - raise ValueError(f'requested {key=} as int cannot be converted to int') from e + raise ValueError(f'Failed to convert {key=} to int') from e - def get_bool(self, key: str, *, default: bool | None = None) -> bool: + def get_bool(self, key: str, *, default: Optional[bool] = None) -> bool: """ Return the bool referred to by the given key if it exists, or the default. Implements :meth:`AbstractConfig.get_bool`. """ if self.config is None: - raise ValueError('attempt to use a closed config') + raise ValueError('Attempt to use a closed config') data = self.__raw_get(key) if data is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default or False return bool(int(data)) - def set(self, key: str, val: int | str | list[str]) -> None: + def set(self, key: str, val: Union[int, str, List[str]]) -> None: """ Set the given key's data to the given value. Implements :meth:`AbstractConfig.set`. """ if self.config is None: - raise ValueError('attempt to use a closed config') - - to_set: str | None = None + raise ValueError('Attempt to use a closed config') if isinstance(val, bool): to_set = str(int(val)) - elif isinstance(val, str): to_set = self.__escape(val) - elif isinstance(val, int): to_set = str(val) - elif isinstance(val, list): to_set = '\n'.join([self.__escape(s) for s in val] + [';']) - else: - raise ValueError(f'Unexpected type for value {type(val)=}') + raise ValueError(f'Unexpected type for value {type(val).__name__}') self.config.set(self.SECTION, key, to_set) self.save() @@ -217,7 +211,7 @@ class LinuxConfig(AbstractConfig): Implements :meth:`AbstractConfig.delete`. """ if self.config is None: - raise ValueError('attempt to use a closed config') + raise ValueError('Attempt to delete from a closed config') self.config.remove_option(self.SECTION, key) self.save() @@ -229,7 +223,7 @@ class LinuxConfig(AbstractConfig): Implements :meth:`AbstractConfig.save`. """ if self.config is None: - raise ValueError('attempt to use a closed config') + raise ValueError('Attempt to save a closed config') with open(self.filename, 'w', encoding='utf-8') as f: self.config.write(f) @@ -241,4 +235,4 @@ class LinuxConfig(AbstractConfig): Implements :meth:`AbstractConfig.close`. """ self.save() - self.config = None + self.config = None # type: ignore diff --git a/config/windows.py b/config/windows.py index f7590a92..94a158f1 100644 --- a/config/windows.py +++ b/config/windows.py @@ -1,6 +1,10 @@ -"""Windows config implementation.""" +""" +windows.py - Windows config implementation. -# spell-checker: words folderid deps hkey edcd +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" import ctypes import functools import pathlib @@ -9,7 +13,6 @@ import uuid import winreg from ctypes.wintypes import DWORD, HANDLE from typing import List, Literal, Optional, Union - from config import AbstractConfig, applongname, appname, logger, update_interval assert sys.platform == 'win32' @@ -43,7 +46,8 @@ class WinConfig(AbstractConfig): """Implementation of AbstractConfig for Windows.""" def __init__(self, do_winsparkle=True) -> None: - self.app_dir_path = pathlib.Path(str(known_folder_path(FOLDERID_LocalAppData))) / appname + super().__init__() + self.app_dir_path = pathlib.Path(known_folder_path(FOLDERID_LocalAppData)) / appname # type: ignore self.app_dir_path.mkdir(exist_ok=True) self.plugin_dir_path = self.app_dir_path / 'plugins' @@ -52,19 +56,17 @@ class WinConfig(AbstractConfig): if getattr(sys, 'frozen', False): self.respath_path = pathlib.Path(sys.executable).parent self.internal_plugin_dir_path = self.respath_path / 'plugins' - else: self.respath_path = pathlib.Path(__file__).parent.parent self.internal_plugin_dir_path = self.respath_path / 'plugins' self.home_path = pathlib.Path.home() - journal_dir_str = known_folder_path(FOLDERID_SavedGames) - journaldir = pathlib.Path(journal_dir_str) if journal_dir_str is not None else None - self.default_journal_dir_path = None # type: ignore - if journaldir is not None: - self.default_journal_dir_path = journaldir / 'Frontier Developments' / 'Elite Dangerous' + journal_dir_path = pathlib.Path( + known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore + self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore + REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806 create_key_defaults = functools.partial( winreg.CreateKeyEx, key=winreg.HKEY_CURRENT_USER, @@ -72,20 +74,18 @@ class WinConfig(AbstractConfig): ) try: - self.__reg_handle: winreg.HKEYType = create_key_defaults( - sub_key=r'Software\Marginal\EDMarketConnector' - ) + self.__reg_handle: winreg.HKEYType = create_key_defaults(sub_key=REGISTRY_SUBKEY) if do_winsparkle: self.__setup_winsparkle() except OSError: - logger.exception('could not create required registry keys') + logger.exception('Could not create required registry keys') raise self.identifier = applongname if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir(): docs = known_folder_path(FOLDERID_Documents) - self.set('outdir', docs if docs is not None else self.home) + self.set("outdir", docs if docs is not None else self.home) def __setup_winsparkle(self): """Ensure the necessary Registry keys for WinSparkle are present.""" @@ -94,31 +94,29 @@ class WinConfig(AbstractConfig): key=winreg.HKEY_CURRENT_USER, access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY, ) - try: - edcd_handle: winreg.HKEYType = create_key_defaults(sub_key=r'Software\EDCD\EDMarketConnector') - winsparkle_reg: winreg.HKEYType = winreg.CreateKeyEx( - edcd_handle, sub_key='WinSparkle', access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY - ) + try: + with create_key_defaults(sub_key=r'Software\EDCD\EDMarketConnector') as edcd_handle: + with winreg.CreateKeyEx(edcd_handle, sub_key='WinSparkle', + access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY) as winsparkle_reg: + # Set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings + UPDATE_INTERVAL_NAME = 'UpdateInterval' # noqa: N806 + CHECK_FOR_UPDATES_NAME = 'CheckForUpdates' # noqa: N806 + REG_SZ = winreg.REG_SZ # noqa: N806 + + winreg.SetValueEx(winsparkle_reg, UPDATE_INTERVAL_NAME, REG_RESERVED_ALWAYS_ZERO, REG_SZ, + str(update_interval)) + + try: + winreg.QueryValueEx(winsparkle_reg, CHECK_FOR_UPDATES_NAME) + except FileNotFoundError: + # Key doesn't exist, set it to a default + winreg.SetValueEx(winsparkle_reg, CHECK_FOR_UPDATES_NAME, REG_RESERVED_ALWAYS_ZERO, REG_SZ, + '1') except OSError: - logger.exception('could not open WinSparkle handle') + logger.exception('Could not open WinSparkle handle') raise - # set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings - winreg.SetValueEx( - winsparkle_reg, 'UpdateInterval', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, str(update_interval) - ) - - try: - winreg.QueryValueEx(winsparkle_reg, 'CheckForUpdates') - - except FileNotFoundError: - # Key doesn't exist, set it to a default - winreg.SetValueEx(winsparkle_reg, 'CheckForUpdates', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, '1') - - winsparkle_reg.Close() - edcd_handle.Close() - def __get_regentry(self, key: str) -> Union[None, list, str, int]: """Access the Registry for the raw entry.""" try: @@ -127,22 +125,20 @@ class WinConfig(AbstractConfig): # Key doesn't exist return None - # The type returned is actually as we'd expect for each of these. The casts are here for type checkers and # For programmers who want to actually know what is going on if _type == winreg.REG_SZ: return str(value) - elif _type == winreg.REG_DWORD: + if _type == winreg.REG_DWORD: return int(value) - elif _type == winreg.REG_MULTI_SZ: + if _type == winreg.REG_MULTI_SZ: return list(value) - else: - logger.warning(f'registry key {key=} returned unknown type {_type=} {value=}') - return None + logger.warning(f'Registry key {key=} returned unknown type {_type=} {value=}') + return None - def get_str(self, key: str, *, default: str | None = None) -> str: + def get_str(self, key: str, *, default: Optional[str] = None) -> str: """ Return the string referred to by the given key if it exists, or the default. @@ -152,12 +148,12 @@ class WinConfig(AbstractConfig): if res is None: return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default - elif not isinstance(res, str): + if not isinstance(res, str): raise ValueError(f'Data from registry is not a string: {type(res)=} {res=}') return res - def get_list(self, key: str, *, default: list | None = None) -> list: + def get_list(self, key: str, *, default: Optional[list] = None) -> list: """ Return the list referred to by the given key if it exists, or the default. @@ -167,7 +163,7 @@ class WinConfig(AbstractConfig): if res is None: return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default - elif not isinstance(res, list): + if not isinstance(res, list): raise ValueError(f'Data from registry is not a list: {type(res)=} {res}') return res @@ -187,7 +183,7 @@ class WinConfig(AbstractConfig): return res - def get_bool(self, key: str, *, default: bool | None = None) -> bool: + def get_bool(self, key: str, *, default: Optional[bool] = None) -> bool: """ Return the bool referred to by the given key if it exists, or the default. @@ -195,7 +191,7 @@ class WinConfig(AbstractConfig): """ res = self.get_int(key, default=default) # type: ignore if res is None: - return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default + return default # Yes it could be None, but we're _assuming_ that people gave us a default return bool(res) @@ -206,12 +202,11 @@ class WinConfig(AbstractConfig): Implements :meth:`AbstractConfig.set`. """ # These are the types that winreg.REG_* below resolve to. - reg_type: Literal[1] | Literal[4] | Literal[7] + reg_type: Union[Literal[1], Literal[4], Literal[7]] if isinstance(val, str): reg_type = winreg.REG_SZ - winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, val) - elif isinstance(val, int): # The original code checked for numbers.Integral, I dont think that is needed. + elif isinstance(val, int): reg_type = winreg.REG_DWORD elif isinstance(val, list): @@ -224,7 +219,6 @@ class WinConfig(AbstractConfig): else: raise ValueError(f'Unexpected type for value {type(val)=}') - # Its complaining about the list, it works, tested on windows, ignored. winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, reg_type, val) # type: ignore def delete(self, key: str, *, suppress=False) -> None: