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:
parent
ff668eab8b
commit
239c5b6e24
@ -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()
|
||||
|
@ -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
|
||||
|
136
config/linux.py
136
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
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user