1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-13 07:47:14 +03:00

[2051] Config Files

This commit is contained in:
David Sangrey 2023-10-19 19:24:43 -04:00
parent ff668eab8b
commit 239c5b6e24
No known key found for this signature in database
GPG Key ID: 3AEADBB0186884BC
4 changed files with 156 additions and 174 deletions

View File

@ -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'
# <https://semver.org/#semantic-versioning-specification-semver>
# 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()

View File

@ -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

View File

@ -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

View File

@ -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: