Skip to content

Commit

Permalink
Rewrite config
Browse files Browse the repository at this point in the history
- Typed config
- Separate sections in object
- Easy and clean interface
- Reusable
  • Loading branch information
Root-Core committed Dec 17, 2024
1 parent a51a512 commit 02a0213
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 49 deletions.
60 changes: 16 additions & 44 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,18 @@
"""Load configuration settings for protonfixes"""

import os
from configparser import ConfigParser

try:
from .logger import log
except ImportError:
from logger import log


CONF_FILE = '~/.config/protonfixes/config.ini'
DEFAULT_CONF = """
[main]
enable_checks = true
enable_splash = false
enable_global_fixes = true
[path]
cache_dir = ~/.cache/protonfixes
"""

CONF = ConfigParser()
CONF.read_string(DEFAULT_CONF)

try:
CONF.read(os.path.expanduser(CONF_FILE))

except Exception:
log.debug('Unable to read config file ' + CONF_FILE)


def opt_bool(opt: str) -> bool:
"""Convert bool ini strings to actual boolean values"""
return opt.lower() in ['yes', 'y', 'true', '1']


locals().update({x: opt_bool(y) for x, y in CONF['main'].items() if 'enable' in x})

locals().update({x: os.path.expanduser(y) for x, y in CONF['path'].items()})

try:
[os.makedirs(os.path.expanduser(d)) for n, d in CONF['path'].items()]
except OSError:
pass
from config_base import ConfigBase
from dataclasses import dataclass
from pathlib import Path

class Config(ConfigBase):
@dataclass
class MainSection:
enable_checks: bool = True
enable_splash: bool = False
enable_global_fixes: bool = True

@dataclass
class PathSection:
cache_dir: Path = Path.home() / '.cache/protonfixes'

config = Config(Path.home() / '.config/protonfixes/config.ini')
123 changes: 123 additions & 0 deletions config_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Load configuration settings for protonfixes"""

import re

from configparser import ConfigParser
from dataclasses import dataclass, is_dataclass
from pathlib import Path

from logger import log, LogLevelType

class ConfigBase:
__CAMEL_CASE_PATTERN: re.Pattern = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')

@classmethod
def snake_case(cls, input: str) -> str:
# Convert CamelCase to snake_case
return cls.__CAMEL_CASE_PATTERN.sub(r'_\1', input).lower()


@staticmethod
def __log(message: str, level: LogLevelType = 'INFO') -> None:
log.log(f'[CONFIG]: {message}', level)


def __init__(self, path: Path) -> None:
assert path
if path.is_file():
self.parse_config_file(path)
elif not path.exists():
self.init_sections()
self.write_config_file(path)
else:
raise IsADirectoryError(f'Given path "{path.absolute()}" exists, but is not a file.')


def init_sections(self, force: bool = False) -> None:
for (member_name, member) in self.__class__.__dict__.items():
# Find non private section definitions
if not member_name.endswith('Section') or member_name.startswith('_'):
continue
if not is_dataclass(member):
continue

# Convert section definition class name to variable name (MyCfgSection -> my_cfg)
section_name = member_name.removesuffix('Section')
section_name = self.snake_case(section_name)

# Do not override existing members by default
if hasattr(self, section_name) and not force:
continue

# Initialize section class as a member
setattr(self, section_name, member())


def parse_config_file(self, file: Path) -> bool:
# Initialize / reset sections to defaults
self.init_sections(True)

# Only precede if the config file exists
if not file.is_file():
return False

try:
parser = ConfigParser()
parser.read(file)

# Iterate over local config section objects
for (section_name, section) in self.__dict__.items():
if not parser.has_section(section_name):
continue

parser_items = parser[section_name]

# Iterate over the option objects in this section
for (option_name, option_item) in section.__dict__.items():
# Match type of local object
match type(option_item).__name__:
case 'int':
setattr(section, option_name, parser_items.getint(option_name))
case 'float':
setattr(section, option_name, parser_items.getfloat(option_name))
case 'bool':
setattr(section, option_name, parser_items.getboolean(option_name))
case 'Path':
setattr(section, option_name, Path(parser_items.get(option_name)))
case 'PosixPath':
setattr(section, option_name, Path(parser_items.get(option_name)))
case 'str':
setattr(section, option_name, parser_items.get(option_name))
case _:
setattr(section, option_name, parser_items.get(option_name))
self.__log(f'[CONFIG]: Type mismatch')
except Exception as ex:
self.__log(f'Failed to parse config file "{file}". Exception: "{ex}"', 'CRIT')
return False
return True


def write_config_file(self, file: Path) -> bool:
# Only precede if the parent directory exists
if not file.parent.is_dir():
self.__log(f'Parent directory "{file.parent}" does not exist. Abort.', 'WARN')
return False

# Create and populate ConfigParser
try:
parser = ConfigParser()
# Iterate over local config section objects
for (section_name, section_item) in self.__dict__.items():
if not parser.has_section(section_name):
parser.add_section(section_name)

for (option_name, option_item) in section_item.__dict__.items():
parser.set(section_name, option_name, str(option_item))

# Write config file
with file.open(mode='w') as stream:
parser.write(stream)
except Exception as ex:
self.__log(f'Failed to create config file "{file}". Exception: "{ex}"', 'CRIT')
return False
return True
10 changes: 5 additions & 5 deletions fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
from typing import Optional

try:
from . import config
from .config import config
from .checks import run_checks
from .logger import log
except ImportError:
import config
from config import config
from checks import run_checks
from logger import log

Expand Down Expand Up @@ -174,15 +174,15 @@ def run_fix(game_id: str) -> None:
if game_id is None:
return

if config.enable_checks:
if config.main.enable_checks:
run_checks()

# execute default.py (local)
if not _run_fix_local(game_id, True) and config.enable_global_fixes:
if not _run_fix_local(game_id, True) and config.main.enable_global_fixes:
_run_fix(game_id, True) # global

# execute <game_id>.py (local)
if not _run_fix_local(game_id, False) and config.enable_global_fixes:
if not _run_fix_local(game_id, False) and config.main.enable_global_fixes:
_run_fix(game_id, False) # global


Expand Down

0 comments on commit 02a0213

Please sign in to comment.