Skip to content

Commit

Permalink
[2051] Revert Linux Changes
Browse files Browse the repository at this point in the history
I can't adequately test this right now, so not touching it.
  • Loading branch information
Rixxan committed Nov 10, 2023
1 parent 28e10b7 commit cb4a261
Showing 1 changed file with 69 additions and 58 deletions.
127 changes: 69 additions & 58 deletions config/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import pathlib
import sys
from configparser import ConfigParser
from typing import Optional, Union, List
from config import AbstractConfig, appname, logger

assert sys.platform == 'linux'
Expand All @@ -19,97 +18,100 @@ 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: Optional[str] = None) -> None:
"""
Initialize LinuxConfig instance.
:param filename: Optional file name to use for configuration storage.
"""
def __init__(self, filename: str | None = None) -> None:
super().__init__()

# Initialize directory paths
# http://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
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 = 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)

# Initialize the configuration
self.config = ConfigParser(comment_prefixes=('#',), interpolation=None)
self.config.read(self.filename)
self.config: ConfigParser | None = ConfigParser(comment_prefixes=('#',), interpolation=None)
self.config.read(self.filename) # read() ignores files that dont exist

# 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")
self.config[self.SECTION].get("this_does_not_exist", fallback=None)
except KeyError:
logger.info("Config section not found. Backing up existing file (if any) and re-adding a section header")
logger.info("Config section not found. Backing up existing file (if any) and readding a section header")
if self.filename.exists():
backup_filename = self.filename.parent / f'{appname}.ini.backup'
backup_filename.write_bytes(self.filename.read_bytes())
(self.filename.parent / f'{appname}.ini.backup').write_bytes(self.filename.read_bytes())

self.config.add_section(self.SECTION)

# Set 'outdir' if not specified or invalid
outdir = self.get_str('outdir')
if outdir is None or not pathlib.Path(outdir).is_dir():
if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir():
self.set('outdir', self.home)

def __escape(self, s: str) -> str:
"""
Escape special characters in a string.
Escape a string using self.__escape_lut.
:param s: The input string.
:return: The escaped string.
"""
escaped_chars = []
This does NOT support multi-character escapes.
:param s: str - String to be escaped.
:return: str - The escaped string.
"""
out = ""
for c in s:
escaped_chars.append(self.__escape_lut.get(c, c))
if c not in self.__escape_lut:
out += c
continue

out += '\\' + self.__escape_lut[c]

return ''.join(escaped_chars)
return out

def __unescape(self, s: str) -> str:
"""
Unescape special characters in a string.
Unescape a string.
:param s: The input string.
:return: The unescaped string.
:param s: str - The string to unescape.
:return: str - The unescaped string.
"""
unescaped_chars = []
out: list[str] = []
i = 0
while i < len(s):
current_char = s[i]
if current_char != '\\':
unescaped_chars.append(current_char)
c = s[i]
if c != '\\':
out.append(c)
i += 1
continue

if i == len(s) - 1:
# We have a backslash, check what its escaping
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]}')

unescaped_chars.append(unescaped)
out.append(unescaped)
i += 2

return "".join(unescaped_chars)
return "".join(out)

def __raw_get(self, key: str) -> Optional[str]:
def __raw_get(self, key: str) -> str | None:
"""
Get a raw data value from the config file.
Expand All @@ -121,36 +123,37 @@ def __raw_get(self, key: str) -> Optional[str]:

return self.config[self.SECTION].get(key)

def get_str(self, key: str, *, default: Optional[str] = None) -> str:
def get_str(self, key: str, *, default: str | None = None) -> str:
"""
Return the string referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_str`.
"""
data = self.__raw_get(key)
if data is None:
return default or ""
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default

if '\n' in data:
raise ValueError('Expected string, but got list')
raise ValueError('asked for string, got list')

return self.__unescape(data)

def get_list(self, key: str, *, default: Optional[list] = None) -> list:
def get_list(self, key: str, *, default: list | None = 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 or []
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default

split = data.split('\n')
if split[-1] != ';':
raise ValueError('Encoded list does not have trailer sentinel')

return [self.__unescape(item) for item in split[:-1]]
return list(map(self.__unescape, split[:-1]))

def get_int(self, key: str, *, default: int = 0) -> int:
"""
Expand All @@ -159,47 +162,55 @@ def get_int(self, key: str, *, default: int = 0) -> int:
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'Failed to convert {key=} to int') from e
raise ValueError(f'requested {key=} as int cannot be converted to int') from e

def get_bool(self, key: str, *, default: Optional[bool] = None) -> bool:
def get_bool(self, key: str, *, default: bool | None = 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 or False
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default

return bool(int(data))

def set(self, key: str, val: Union[int, str, List[str]]) -> None:
def set(self, key: str, val: 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')
raise ValueError('attempt to use a closed config')

to_set: str | None = None
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).__name__}')
raise ValueError(f'Unexpected type for value {type(val)=}')

self.config.set(self.SECTION, key, to_set)
self.save()
Expand All @@ -211,7 +222,7 @@ def delete(self, key: str, *, suppress=False) -> None:
Implements :meth:`AbstractConfig.delete`.
"""
if self.config is None:
raise ValueError('Attempt to delete from a closed config')
raise ValueError('attempt to use a closed config')

self.config.remove_option(self.SECTION, key)
self.save()
Expand All @@ -223,7 +234,7 @@ def save(self) -> None:
Implements :meth:`AbstractConfig.save`.
"""
if self.config is None:
raise ValueError('Attempt to save a closed config')
raise ValueError('attempt to use a closed config')

with open(self.filename, 'w', encoding='utf-8') as f:
self.config.write(f)
Expand All @@ -235,4 +246,4 @@ def close(self) -> None:
Implements :meth:`AbstractConfig.close`.
"""
self.save()
self.config = None # type: ignore
self.config = None

0 comments on commit cb4a261

Please sign in to comment.