|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | + |
| 4 | +import pathlib |
| 5 | +import configparser |
| 6 | +import logging |
| 7 | +import sys |
| 8 | +import traceback |
| 9 | +import re |
| 10 | +import dataclasses |
| 11 | +import urllib.parse |
| 12 | +from collections import OrderedDict |
| 13 | +# Own modules |
| 14 | +from pyrtfparse import errors |
| 15 | +from pyrtfparse import utils |
| 16 | +from pyrtfparse import menu |
| 17 | + |
| 18 | + |
| 19 | +# setup logging |
| 20 | +logger = logging.getLogger(__name__) |
| 21 | + |
| 22 | + |
| 23 | +@dataclasses.dataclass |
| 24 | +class Preconfigured_Path(): |
| 25 | + internal_name: str |
| 26 | + path: dataclasses.InitVar[pathlib.Path] |
| 27 | + comment: str |
| 28 | + def __post_init__(self, path) -> None: |
| 29 | + self.path = pathlib.Path(path) |
| 30 | + |
| 31 | + |
| 32 | +class Config(): |
| 33 | + """ |
| 34 | + Holds all confituration data readily available as attributes |
| 35 | + """ |
| 36 | + def __init__(self, cfg_path: pathlib.Path, autoconfig: bool) -> None: |
| 37 | + self.error_regex = re.compile(r"""config_parser\.get.*\(["'](?P<section>\w+)["'], *["'](?P<variable>\w+)["']\)""") |
| 38 | + self.path_to_config_file = cfg_path |
| 39 | + self.path_to_home = utils.provide_dir(self.path_to_config_file.parent) |
| 40 | + self.path_to_pyrtfparse_home = pathlib.Path.home() / utils.home_dir_name |
| 41 | + self._subdir_dir = Preconfigured_Path( |
| 42 | + internal_name="subdir_dir", |
| 43 | + path=self.path_to_pyrtfparse_home / "subdir", |
| 44 | + comment="some subdir", |
| 45 | + ) |
| 46 | + self._wizard_has_run = False |
| 47 | + self.autoconfig = autoconfig |
| 48 | + self.read_config_file() |
| 49 | + self.check_paths = (self._subdir_dir, |
| 50 | + ) |
| 51 | + self.integrity_check() |
| 52 | + def __enter__(self): |
| 53 | + return self |
| 54 | + def __exit__(self, exc_type, exc_value, traceback) -> None: |
| 55 | + # Deleting config during development. Comment after release. |
| 56 | + # self.delete_config_file() |
| 57 | + pass |
| 58 | + def reset_parser(self) -> None: |
| 59 | + self.config_parser = configparser.ConfigParser(allow_no_value=True) |
| 60 | + self.config_parser.optionxform = str |
| 61 | + def read_config_file(self) -> None: |
| 62 | + """ |
| 63 | + Reads current configuration file or creates a new one with a default configuration |
| 64 | + """ |
| 65 | + self.reset_parser() |
| 66 | + try: |
| 67 | + self.config_parser.read_file(open(self.path_to_config_file)) |
| 68 | + self.parse() |
| 69 | + logger.debug(f"{self.path_to_config_file.name} read") |
| 70 | + except FileNotFoundError: |
| 71 | + logger.info("Config file missing, creating new default config file") |
| 72 | + self.create_config_file() |
| 73 | + def integrity_check(self) -> None: |
| 74 | + try: |
| 75 | + for preconf_path in self.check_paths: |
| 76 | + path_to_check = preconf_path.path |
| 77 | + assert path_to_check.exists() |
| 78 | + except AssertionError as e: |
| 79 | + logger.debug(f"Path not found, starting wizard") |
| 80 | + self.wizard(errors.WrongConfiguration(f"{self.path_to_config_file.name}: '{str(path_to_check)}', path does not exist!", preconf_path), autoconfig=self.autoconfig) |
| 81 | + def create_config_file(self) -> None: |
| 82 | + """ |
| 83 | + Creates the default config file |
| 84 | + """ |
| 85 | + self.reset_parser() |
| 86 | + self.config_parser.add_section("Paths") |
| 87 | + self.config_parser.set("Paths", "# You can write paths in Windows format or Linux/POSIX format.") |
| 88 | + self.config_parser.set("Paths", "# A trailing '/' at the end of the final directory in a POSIX path") |
| 89 | + self.config_parser.set("Paths", "# or a '\\' at the end of the final directory of a Windows path") |
| 90 | + self.config_parser.set("Paths", "# does not interfere with the path parser.") |
| 91 | + self.config_parser.set("Paths", "") |
| 92 | + for preconf_path in ( |
| 93 | + self._subdir_dir, |
| 94 | + ): |
| 95 | + self.config_parser.set("Paths", f"# {preconf_path.comment[0].capitalize()}{preconf_path.comment[1:]}") |
| 96 | + self.config_parser.set("Paths", f"{preconf_path.internal_name}", f"{preconf_path.path}") |
| 97 | + with open(self.path_to_config_file, mode="w", encoding="utf-8") as configfh: |
| 98 | + self.config_parser.write(configfh) |
| 99 | + self.read_config_file() |
| 100 | + def delete_config_file(self) -> None: |
| 101 | + """ |
| 102 | + Serves debugging purposes. Deletes the config file. |
| 103 | + """ |
| 104 | + try: |
| 105 | + self.path_to_config_file.unlink() |
| 106 | + logger.info(f"{self.path_to_config_file.name} deleted") |
| 107 | + except FileNotFoundError as exc: |
| 108 | + logger.error(f"Could not delete {self.path_to_config_file.name} because it does not exist") |
| 109 | + def getpath(self, section: str, value: str) -> pathlib.Path: |
| 110 | + """ |
| 111 | + Returns value from config file as pathlib.Path object |
| 112 | + """ |
| 113 | + return pathlib.Path(self.config_parser.get(section, value)) |
| 114 | + def geturl(self, section: str, raw_url: str) -> urllib.parse.ParseResult: |
| 115 | + """ |
| 116 | + Parses a URL and returns urllib ParseResult |
| 117 | + """ |
| 118 | + return urllib.parse.urlparse(self.config_parser.get(section, raw_url)) |
| 119 | + def parse(self) -> None: |
| 120 | + """ |
| 121 | + Parses the configuration files into usable attributes |
| 122 | + """ |
| 123 | + try: |
| 124 | + self.subdir_dir = self.getpath("Paths", "subdir_dir") |
| 125 | + except ValueError: |
| 126 | + exc_type, exc_value, exc_traceback = sys.exc_info() |
| 127 | + lines = traceback.format_exc().splitlines() |
| 128 | + section, variable, value = False, False, False |
| 129 | + for line in lines: |
| 130 | + if "config_parser" in line: |
| 131 | + match = re.search(self.error_regex, line) |
| 132 | + if match: |
| 133 | + section = match.group("section") |
| 134 | + variable = match.group("variable") |
| 135 | + value = lines[-1].split()[-1] |
| 136 | + if section and variable and value: |
| 137 | + message = f"{self.path_to_config_file.name}: '{variable}' in section '{section}' has an unacceptable value of {value}" |
| 138 | + raise errors.WrongConfiguration(message, None) |
| 139 | + else: |
| 140 | + raise |
| 141 | + except configparser.NoOptionError as err: |
| 142 | + raise errors.WrongConfiguration(f"{self.path_to_config_file.name}: {err.message}", err) |
| 143 | + except configparser.NoSectionError as err: |
| 144 | + raise errors.WrongConfiguration(f"{self.path_to_config_file.name}: {err.message}", err) |
| 145 | + except Exception as err: |
| 146 | + raise errors.WrongConfiguration(f"There is something wrong with {self.path_to_config_file.name}. Please check it carefully or delete it to have it recreated.", err) |
| 147 | + def configure_paths(self, preconf_path: Preconfigured_Path, manually: bool) -> None: |
| 148 | + logger.debug(f"{preconf_path.internal_name} needs to be reconfigured") |
| 149 | + if manually: |
| 150 | + logger.debug(f"Configuring paths manually") |
| 151 | + while True: |
| 152 | + try: |
| 153 | + path_to_create = utils.input_path(f"Input a {preconf_path.comment}: ") |
| 154 | + created_path = utils.provide_dir(path_to_create) |
| 155 | + break |
| 156 | + except OSError as err: |
| 157 | + logger.error(err) |
| 158 | + continue |
| 159 | + else: |
| 160 | + logger.debug(f"Configuring paths automatically") |
| 161 | + created_path = utils.provide_dir(preconf_path.path) |
| 162 | + preconf_path.path = created_path |
| 163 | + self.create_config_file() |
| 164 | + self.integrity_check() |
| 165 | + def wizard(self, error: errors.WrongConfiguration, autoconfig: bool) -> None: |
| 166 | + """ |
| 167 | + Configuration wizard guides the user through the initial setup process |
| 168 | + """ |
| 169 | + wiz_menu = menu.Text_Menu(menu_name="Configuration Wizard", heading=r""" |
| 170 | + ____ ____ __ _ ____ _ ____ _ _ ____ ____ ___ _ ____ __ _ |
| 171 | + |___ [__] | \| |--- | |__, |__| |--< |--| | | [__] | \| |
| 172 | + _ _ _ ___ ____ ____ ___ |
| 173 | + |/\| | /__ |--| |--< |__> |
| 174 | + """) |
| 175 | + if not self._wizard_has_run: |
| 176 | + wiz_menu.show_heading() |
| 177 | + self._wizard_has_run = True |
| 178 | + if "path does not exist" in error.message: |
| 179 | + reason = (f"{error.payload.internal_name} ({error.payload.path}) does not exist!") |
| 180 | + options = OrderedDict(( |
| 181 | + ("A", "Automatically configure this and all remaining claws settings"), |
| 182 | + ("C", "Create this path automatically"), |
| 183 | + ("M", "Manually input correct path to use or to create"), |
| 184 | + ("Q", f"Quit and edit `{error.payload.internal_name}` in {self.path_to_config_file.name}"), |
| 185 | + )) |
| 186 | + if autoconfig: |
| 187 | + choice = "C" |
| 188 | + else: |
| 189 | + choice = None |
| 190 | + if not choice: |
| 191 | + wiz_menu.show_reason(reason) |
| 192 | + choice = wiz_menu.choose_from(options) |
| 193 | + if choice == "A": |
| 194 | + self.autoconfig = True |
| 195 | + logger.debug(f"Your choice: {choice}") |
| 196 | + if choice == "C": |
| 197 | + self.configure_paths(error.payload, manually=False) |
| 198 | + elif choice == "M": |
| 199 | + self.configure_paths(error.payload, manually=True) |
| 200 | + elif choice == "Q": |
| 201 | + raise errors.WrongConfiguration(f"Who needs a wizard, when you can edit `{self.path_to_config_file.name}` yourself, right?", None) |
| 202 | + self.integrity_check() |
| 203 | + else: |
| 204 | + raise NotImplementedError(f"Starting configuration wizard with {err.message} is not implemented yet") |
| 205 | + |
| 206 | + |
| 207 | +if __name__ == "__main__": |
| 208 | + pass |
0 commit comments