Source code for pg4n.config_parser

import re
import sys
from dataclasses import dataclass
from typing import Optional, TextIO

from .config_values import ConfigValues


[docs]class ConfigParser: _option_matcher: re.Pattern = re.compile( r"\s*(?P<optname>\w+)\s+(?P<optval>1|0|true|false|yes|no)\s*$", flags=re.IGNORECASE, ) _empty_line_matcher: re.Pattern = re.compile(r"^\s*$") _comment_matcher: re.Pattern = re.compile(r"^\s*#+.*$") def __init__(self, file: TextIO): self.file: TextIO = file
[docs] def parse(self) -> Optional[ConfigValues]: """ Reads config values from file givein in __init__. """ optnames = [x.lower() for x in ConfigValues.__annotations__.keys()] config_values: ConfigValues = {} # Needed for bytes containing files self.file.seek(0) # The following stuff is for improved error messages @dataclass(frozen=True) class SeenOptionContext: key: str line: str line_number: int seen_option_contexts: list[SeenOptionContext] = [] multiply_defined_options = set() for line_number, line in enumerate(self.file.readlines()): # To make this work with bytes objects like TemporaryFile contents if isinstance(line, bytes): line = bytes.decode(line, "utf-8") line = str(line) if match := ConfigParser._empty_line_matcher.match(line): continue if match := ConfigParser._comment_matcher.match(line): continue if match := ConfigParser._option_matcher.match(line): optname = match.group("optname") if optname.lower() in optnames: key = self._convert_from_anycase_to_propercase(optname) config_values[key] = self._optval_to_bool( str(match.group("optval")) ) if key in [x.key for x in seen_option_contexts]: seen_option_contexts.append( SeenOptionContext( key=key, line=line.rstrip("\n"), line_number=line_number ) ) multiply_defined_options.add(key) else: seen_option_contexts.append( SeenOptionContext( key=key, line=line.rstrip("\n"), line_number=line_number ) ) else: output_line = line.rstrip("\n") print( f"warning: bad warning name or value in line {line_number}: '{output_line}' in configuration file: '{self.file.name}'", file=sys.stderr, ) else: output_line = line.rstrip("\n") print( f"warning: unable to parse line {line_number}: '{output_line}' in configuration file: '{self.file.name}'", file=sys.stderr, ) # Not so brilliant computational complexity for key in multiply_defined_options: seen_option_lines = "" for ctx in filter(lambda x: x.key == key, seen_option_contexts): seen_option_lines += f"line {ctx.line_number}: '{ctx.line}'\n" print( f"warning: option '{key}' is set multiple times\n{seen_option_lines}in configuration file: '{self.file.name}'", file=sys.stderr, ) return config_values if len(config_values) > 0 else None
def _optval_to_bool(self, optval: str) -> bool: """ Excepts only valid option values. Converts config file option value string into bool. """ true_values = ("true", "1", "yes") false_values = ("false", "0", "no") if optval.lower() in true_values: return True if optval.lower() in false_values: return False return False def _convert_from_anycase_to_propercase(self, anycase_key: str) -> str: """ Assumes that anycase_key matches some option name case-insensitively. Users can write option values case-insensitively. We still need to convert that user written value to the proper one that matches the fields in ConfigValues class. """ fields = ConfigValues.__annotations__.keys() for field in fields: if anycase_key.lower() == field.lower(): return field