1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-20 02:47:39 +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

@ -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. 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. Linux uses a file, but for commonality it's still a flat data structure.
macOS uses a 'defaults' object. macOS uses a 'defaults' object.
""" """
__all__ = [ __all__ = [
# defined in the order they appear in the file # defined in the order they appear in the file
'GITVERSION_FILE', 'GITVERSION_FILE',
@ -40,10 +42,8 @@ import sys
import traceback import traceback
import warnings import warnings
from abc import abstractmethod 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 import semantic_version
from constants import GITVERSION_FILE, applongname, appname from constants import GITVERSION_FILE, applongname, appname
# Any of these may be imported by plugins # Any of these may be imported by plugins
@ -52,7 +52,7 @@ appcmdname = 'EDMC'
# <https://semver.org/#semantic-versioning-specification-semver> # <https://semver.org/#semantic-versioning-specification-semver>
# Major.Minor.Patch(-prerelease)(+buildmetadata) # Major.Minor.Patch(-prerelease)(+buildmetadata)
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild() # 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 _cached_version: Optional[semantic_version.Version] = None
copyright = '© 2015-2019 Jonathan Harris, 2020-2023 EDCD' 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_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'
update_interval = 8*60*60 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 # 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 # TRACE logging code that should actually be used. Means not spamming it
# *all* if only interested in some things. # *all* if only interested in some things.
trace_on: list[str] = [] trace_on: List[str] = []
capi_pretend_down: bool = False capi_pretend_down: bool = False
capi_debug_access_token: Optional[str] = None capi_debug_access_token: Optional[str] = None
@ -79,7 +79,6 @@ else:
_T = TypeVar('_T') _T = TypeVar('_T')
###########################################################################
def git_shorthash_from_head() -> str: def git_shorthash_from_head() -> str:
""" """
Determine short hash for current git HEAD. Determine short hash for current git HEAD.
@ -91,13 +90,14 @@ def git_shorthash_from_head() -> str:
shorthash: str = None # type: ignore shorthash: str = None # type: ignore
try: try:
git_cmd = subprocess.Popen('git rev-parse --short HEAD'.split(), git_cmd = subprocess.Popen(
stdout=subprocess.PIPE, "git rev-parse --short HEAD".split(),
stderr=subprocess.STDOUT stdout=subprocess.PIPE,
) stderr=subprocess.STDOUT,
)
out, err = git_cmd.communicate() 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}") logger.info(f"Couldn't run git command for short hash: {e!r}")
else: else:
@ -131,7 +131,7 @@ def appversion() -> semantic_version.Version:
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
# Running frozen, so we should have a .gitversion file # Running frozen, so we should have a .gitversion file
# Yes, .parent because if frozen we're inside library.zip # 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() shorthash = gitv.read()
else: else:
@ -157,23 +157,15 @@ def appversion_nobuild() -> semantic_version.Version:
:return: App version without any build meta data. :return: App version without any build meta data.
""" """
return appversion().truncate('prerelease') return appversion().truncate('prerelease')
###########################################################################
class AbstractConfig(abc.ABC): class AbstractConfig(abc.ABC):
"""Abstract root class of all platform specific Config implementations.""" """Abstract root class of all platform specific Config implementations."""
OUT_EDDN_SEND_STATION_DATA = 1 OUT_EDDN_SEND_STATION_DATA = 1
# OUT_MKT_BPC = 2 # No longer supported
OUT_MKT_TD = 4 OUT_MKT_TD = 4
OUT_MKT_CSV = 8 OUT_MKT_CSV = 8
OUT_SHIP = 16 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_MKT_MANUAL = 1024
OUT_EDDN_SEND_NON_STATION = 2048 OUT_EDDN_SEND_NON_STATION = 2048
OUT_EDDN_DELAY = 4096 OUT_EDDN_DELAY = 4096
@ -185,7 +177,6 @@ class AbstractConfig(abc.ABC):
respath_path: pathlib.Path respath_path: pathlib.Path
home_path: pathlib.Path home_path: pathlib.Path
default_journal_dir_path: pathlib.Path default_journal_dir_path: pathlib.Path
identifier: str identifier: str
__in_shutdown = False # Is the application currently shutting down ? __in_shutdown = False # Is the application currently shutting down ?
@ -294,7 +285,7 @@ class AbstractConfig(abc.ABC):
@staticmethod @staticmethod
def _suppress_call( 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 *args: Any, **kwargs: Any
) -> Optional[_T]: ) -> Optional[_T]:
if exceptions is None: if exceptions is None:
@ -303,15 +294,15 @@ class AbstractConfig(abc.ABC):
if not isinstance(exceptions, list): if not isinstance(exceptions, list):
exceptions = [exceptions] exceptions = [exceptions]
with contextlib.suppress(*exceptions): # type: ignore # it works fine, mypy with contextlib.suppress(*exceptions):
return func(*args, **kwargs) return func(*args, **kwargs)
return None return None
def get( def get(
self, key: str, self, key: str,
default: list | str | bool | int | None = None default: Union[list, str, bool, int, None] = None
) -> list | str | bool | int | None: ) -> Union[list, str, bool, int, None]:
""" """
Return the data for the requested key, or a default. 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: if (a_list := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None:
return a_list 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 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 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 an_int
return default # type: ignore return default
@abstractmethod @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. 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 raise NotImplementedError
@abstractmethod @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. 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 raise NotImplementedError
@abstractmethod @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. 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 raise NotImplementedError
@abstractmethod @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. Set the given key's data to the given value.
@ -462,16 +453,15 @@ def get_config(*args, **kwargs) -> AbstractConfig:
from .darwin import MacConfig from .darwin import MacConfig
return MacConfig(*args, **kwargs) return MacConfig(*args, **kwargs)
elif sys.platform == "win32": # pragma: sys-platform-win32 if sys.platform == "win32": # pragma: sys-platform-win32
from .windows import WinConfig from .windows import WinConfig
return WinConfig(*args, **kwargs) return WinConfig(*args, **kwargs)
elif sys.platform == "linux": # pragma: sys-platform-linux if sys.platform == "linux": # pragma: sys-platform-linux
from .linux import LinuxConfig from .linux import LinuxConfig
return LinuxConfig(*args, **kwargs) 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() config = get_config()

@ -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 pathlib
import sys import sys
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
from Foundation import ( # type: ignore from Foundation import ( # type: ignore
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults, NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults,
NSUserDomainMask NSUserDomainMask
) )
from config import AbstractConfig, appname, logger from config import AbstractConfig, appname, logger
assert sys.platform == 'darwin' assert sys.platform == 'darwin'
@ -82,7 +86,7 @@ class MacConfig(AbstractConfig):
""" """
res = self.__raw_get(key) res = self.__raw_get(key)
if res is None: 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): if not isinstance(res, str):
raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}') raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}')
@ -97,9 +101,9 @@ class MacConfig(AbstractConfig):
""" """
res = self.__raw_get(key) res = self.__raw_get(key)
if res is None: 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}') raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
return res return res
@ -114,7 +118,7 @@ class MacConfig(AbstractConfig):
if res is None: if res is None:
return default 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}') raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
try: try:
@ -122,7 +126,7 @@ class MacConfig(AbstractConfig):
except ValueError as e: except ValueError as e:
logger.error(f'__raw_get returned {res!r} which cannot be parsed to an int: {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: def get_bool(self, key: str, *, default: bool = None) -> bool:
""" """
@ -132,9 +136,9 @@ class MacConfig(AbstractConfig):
""" """
res = self.__raw_get(key) res = self.__raw_get(key)
if res is None: 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}') raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
return res return res

@ -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 os
import pathlib import pathlib
import sys import sys
from configparser import ConfigParser from configparser import ConfigParser
from typing import Optional, Union, List
from config import AbstractConfig, appname, logger from config import AbstractConfig, appname, logger
assert sys.platform == 'linux' assert sys.platform == 'linux'
@ -13,100 +19,97 @@ class LinuxConfig(AbstractConfig):
"""Linux implementation of AbstractConfig.""" """Linux implementation of AbstractConfig."""
SECTION = 'config' SECTION = 'config'
# TODO: I dislike this, would rather use a sane config file format. But here we are.
__unescape_lut = {'\\': '\\', 'n': '\n', ';': ';', 'r': '\r', '#': '#'} __unescape_lut = {'\\': '\\', 'n': '\n', ';': ';', 'r': '\r', '#': '#'}
__escape_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__() 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() 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 = xdg_data_home / appname
self.app_dir_path.mkdir(exist_ok=True, parents=True) self.app_dir_path.mkdir(exist_ok=True, parents=True)
self.plugin_dir_path = self.app_dir_path / 'plugins' self.plugin_dir_path = self.app_dir_path / 'plugins'
self.plugin_dir_path.mkdir(exist_ok=True) self.plugin_dir_path.mkdir(exist_ok=True)
self.respath_path = pathlib.Path(__file__).parent.parent self.respath_path = pathlib.Path(__file__).parent.parent
self.internal_plugin_dir_path = self.respath_path / 'plugins' self.internal_plugin_dir_path = self.respath_path / 'plugins'
self.default_journal_dir_path = None # type: ignore 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() config_home = pathlib.Path(os.getenv('XDG_CONFIG_HOME', default='~/.config')).expanduser()
self.filename = pathlib.Path(filename) if filename is not None else config_home / appname / f'{appname}.ini'
self.filename = config_home / appname / f'{appname}.ini'
if filename is not None:
self.filename = pathlib.Path(filename)
self.filename.parent.mkdir(exist_ok=True, parents=True) self.filename.parent.mkdir(exist_ok=True, parents=True)
self.config: ConfigParser | None = ConfigParser(comment_prefixes=('#',), interpolation=None) # Initialize the configuration
self.config.read(self.filename) # read() ignores files that dont exist 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 # Ensure that our section exists. This is here because configparser will happily create files for us, but it
# does not magically create sections # does not magically create sections
try: try:
self.config[self.SECTION].get("this_does_not_exist", fallback=None) self.config[self.SECTION].get("this_does_not_exist")
except KeyError: 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(): 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) 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) self.set('outdir', self.home)
def __escape(self, s: str) -> str: 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: The input string.
:return: The escaped string.
:param s: str - String to be escaped.
:return: str - The escaped string.
""" """
out = "" escaped_chars = []
for c in s: for c in s:
if c not in self.__escape_lut: escaped_chars.append(self.__escape_lut.get(c, c))
out += c
continue
out += '\\' + self.__escape_lut[c] return ''.join(escaped_chars)
return out
def __unescape(self, s: str) -> str: def __unescape(self, s: str) -> str:
""" """
Unescape a string. Unescape special characters in a string.
:param s: str - The string to unescape. :param s: The input string.
:return: str - The unescaped string. :return: The unescaped string.
""" """
out: list[str] = [] unescaped_chars = []
i = 0 i = 0
while i < len(s): while i < len(s):
c = s[i] current_char = s[i]
if c != '\\': if current_char != '\\':
out.append(c) unescaped_chars.append(current_char)
i += 1 i += 1
continue 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') 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: 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 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. Get a raw data value from the config file.
@ -118,7 +121,7 @@ class LinuxConfig(AbstractConfig):
return self.config[self.SECTION].get(key) 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. 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) data = self.__raw_get(key)
if data is None: 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: if '\n' in data:
raise ValueError('asked for string, got list') raise ValueError('Expected string, but got list')
return self.__unescape(data) 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. Return the list referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_list`. Implements :meth:`AbstractConfig.get_list`.
""" """
data = self.__raw_get(key) data = self.__raw_get(key)
if data is None: 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') split = data.split('\n')
if split[-1] != ';': if split[-1] != ';':
raise ValueError('Encoded list does not have trailer sentinel') 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: def get_int(self, key: str, *, default: int = 0) -> int:
""" """
@ -157,55 +159,47 @@ class LinuxConfig(AbstractConfig):
Implements :meth:`AbstractConfig.get_int`. Implements :meth:`AbstractConfig.get_int`.
""" """
data = self.__raw_get(key) data = self.__raw_get(key)
if data is None: if data is None:
return default return default
try: try:
return int(data) return int(data)
except ValueError as e: 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. Return the bool referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_bool`. Implements :meth:`AbstractConfig.get_bool`.
""" """
if self.config is None: 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) data = self.__raw_get(key)
if data is None: 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)) 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. Set the given key's data to the given value.
Implements :meth:`AbstractConfig.set`. Implements :meth:`AbstractConfig.set`.
""" """
if self.config is None: if self.config is None:
raise ValueError('attempt to use a closed config') raise ValueError('Attempt to use a closed config')
to_set: str | None = None
if isinstance(val, bool): if isinstance(val, bool):
to_set = str(int(val)) to_set = str(int(val))
elif isinstance(val, str): elif isinstance(val, str):
to_set = self.__escape(val) to_set = self.__escape(val)
elif isinstance(val, int): elif isinstance(val, int):
to_set = str(val) to_set = str(val)
elif isinstance(val, list): elif isinstance(val, list):
to_set = '\n'.join([self.__escape(s) for s in val] + [';']) to_set = '\n'.join([self.__escape(s) for s in val] + [';'])
else: 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.config.set(self.SECTION, key, to_set)
self.save() self.save()
@ -217,7 +211,7 @@ class LinuxConfig(AbstractConfig):
Implements :meth:`AbstractConfig.delete`. Implements :meth:`AbstractConfig.delete`.
""" """
if self.config is None: 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.config.remove_option(self.SECTION, key)
self.save() self.save()
@ -229,7 +223,7 @@ class LinuxConfig(AbstractConfig):
Implements :meth:`AbstractConfig.save`. Implements :meth:`AbstractConfig.save`.
""" """
if self.config is None: 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: with open(self.filename, 'w', encoding='utf-8') as f:
self.config.write(f) self.config.write(f)
@ -241,4 +235,4 @@ class LinuxConfig(AbstractConfig):
Implements :meth:`AbstractConfig.close`. Implements :meth:`AbstractConfig.close`.
""" """
self.save() self.save()
self.config = None self.config = None # type: ignore

@ -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 ctypes
import functools import functools
import pathlib import pathlib
@ -9,7 +13,6 @@ import uuid
import winreg import winreg
from ctypes.wintypes import DWORD, HANDLE from ctypes.wintypes import DWORD, HANDLE
from typing import List, Literal, Optional, Union from typing import List, Literal, Optional, Union
from config import AbstractConfig, applongname, appname, logger, update_interval from config import AbstractConfig, applongname, appname, logger, update_interval
assert sys.platform == 'win32' assert sys.platform == 'win32'
@ -43,7 +46,8 @@ class WinConfig(AbstractConfig):
"""Implementation of AbstractConfig for Windows.""" """Implementation of AbstractConfig for Windows."""
def __init__(self, do_winsparkle=True) -> None: 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.app_dir_path.mkdir(exist_ok=True)
self.plugin_dir_path = self.app_dir_path / 'plugins' self.plugin_dir_path = self.app_dir_path / 'plugins'
@ -52,19 +56,17 @@ class WinConfig(AbstractConfig):
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
self.respath_path = pathlib.Path(sys.executable).parent self.respath_path = pathlib.Path(sys.executable).parent
self.internal_plugin_dir_path = self.respath_path / 'plugins' self.internal_plugin_dir_path = self.respath_path / 'plugins'
else: else:
self.respath_path = pathlib.Path(__file__).parent.parent self.respath_path = pathlib.Path(__file__).parent.parent
self.internal_plugin_dir_path = self.respath_path / 'plugins' self.internal_plugin_dir_path = self.respath_path / 'plugins'
self.home_path = pathlib.Path.home() self.home_path = pathlib.Path.home()
journal_dir_str = known_folder_path(FOLDERID_SavedGames) journal_dir_path = pathlib.Path(
journaldir = pathlib.Path(journal_dir_str) if journal_dir_str is not None else None known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
self.default_journal_dir_path = None # type: ignore self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
if journaldir is not None:
self.default_journal_dir_path = journaldir / 'Frontier Developments' / 'Elite Dangerous'
REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806
create_key_defaults = functools.partial( create_key_defaults = functools.partial(
winreg.CreateKeyEx, winreg.CreateKeyEx,
key=winreg.HKEY_CURRENT_USER, key=winreg.HKEY_CURRENT_USER,
@ -72,20 +74,18 @@ class WinConfig(AbstractConfig):
) )
try: try:
self.__reg_handle: winreg.HKEYType = create_key_defaults( self.__reg_handle: winreg.HKEYType = create_key_defaults(sub_key=REGISTRY_SUBKEY)
sub_key=r'Software\Marginal\EDMarketConnector'
)
if do_winsparkle: if do_winsparkle:
self.__setup_winsparkle() self.__setup_winsparkle()
except OSError: except OSError:
logger.exception('could not create required registry keys') logger.exception('Could not create required registry keys')
raise raise
self.identifier = applongname self.identifier = applongname
if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir(): if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir():
docs = known_folder_path(FOLDERID_Documents) 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): def __setup_winsparkle(self):
"""Ensure the necessary Registry keys for WinSparkle are present.""" """Ensure the necessary Registry keys for WinSparkle are present."""
@ -94,31 +94,29 @@ class WinConfig(AbstractConfig):
key=winreg.HKEY_CURRENT_USER, key=winreg.HKEY_CURRENT_USER,
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY, 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: except OSError:
logger.exception('could not open WinSparkle handle') logger.exception('Could not open WinSparkle handle')
raise 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]: def __get_regentry(self, key: str) -> Union[None, list, str, int]:
"""Access the Registry for the raw entry.""" """Access the Registry for the raw entry."""
try: try:
@ -127,22 +125,20 @@ class WinConfig(AbstractConfig):
# Key doesn't exist # Key doesn't exist
return None 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 # For programmers who want to actually know what is going on
if _type == winreg.REG_SZ: if _type == winreg.REG_SZ:
return str(value) return str(value)
elif _type == winreg.REG_DWORD: if _type == winreg.REG_DWORD:
return int(value) return int(value)
elif _type == winreg.REG_MULTI_SZ: if _type == winreg.REG_MULTI_SZ:
return list(value) return list(value)
else: logger.warning(f'Registry key {key=} returned unknown type {_type=} {value=}')
logger.warning(f'registry key {key=} returned unknown type {_type=} {value=}') return None
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. 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: 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 # 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=}') raise ValueError(f'Data from registry is not a string: {type(res)=} {res=}')
return 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. 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: 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 # 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}') raise ValueError(f'Data from registry is not a list: {type(res)=} {res}')
return res return res
@ -187,7 +183,7 @@ class WinConfig(AbstractConfig):
return res 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. 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 res = self.get_int(key, default=default) # type: ignore
if res is None: 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) return bool(res)
@ -206,12 +202,11 @@ class WinConfig(AbstractConfig):
Implements :meth:`AbstractConfig.set`. Implements :meth:`AbstractConfig.set`.
""" """
# These are the types that winreg.REG_* below resolve to. # 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): if isinstance(val, str):
reg_type = winreg.REG_SZ 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 reg_type = winreg.REG_DWORD
elif isinstance(val, list): elif isinstance(val, list):
@ -224,7 +219,6 @@ class WinConfig(AbstractConfig):
else: else:
raise ValueError(f'Unexpected type for value {type(val)=}') 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 winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, reg_type, val) # type: ignore
def delete(self, key: str, *, suppress=False) -> None: def delete(self, key: str, *, suppress=False) -> None: