pferd/PFERD/config.py

188 lines
5.8 KiB
Python
Raw Normal View History

2021-05-15 21:33:51 +02:00
import asyncio
2021-04-27 12:41:49 +02:00
import os
2021-05-15 21:33:51 +02:00
import sys
2021-05-05 23:45:10 +02:00
from configparser import ConfigParser, SectionProxy
2021-04-27 12:41:49 +02:00
from pathlib import Path
2021-05-05 23:45:10 +02:00
from typing import Any, List, NoReturn, Optional, Tuple
2021-04-27 12:41:49 +02:00
2021-05-23 11:04:50 +02:00
from rich.markup import escape
2021-05-19 18:10:17 +02:00
from .logging import log
from .utils import fmt_real_path, prompt_yes_no
2021-04-27 12:41:49 +02:00
class ConfigLoadError(Exception):
"""
Something went wrong while loading the config from a file.
"""
def __init__(self, path: Path, reason: str):
super().__init__(f"Failed to load config from {fmt_real_path(path)}")
self.path = path
self.reason = reason
2021-04-27 12:41:49 +02:00
class ConfigOptionError(Exception):
"""
An option in the config file has an invalid or missing value.
"""
2021-04-27 12:41:49 +02:00
def __init__(self, section: str, key: str, desc: str):
super().__init__(f"Section {section!r}, key {key!r}: {desc}")
self.section = section
self.key = key
self.desc = desc
2021-04-27 12:41:49 +02:00
class ConfigDumpError(Exception):
def __init__(self, path: Path, reason: str):
super().__init__(f"Failed to dump config to {fmt_real_path(path)}")
self.path = path
self.reason = reason
2021-05-05 23:45:10 +02:00
class Section:
"""
Base class for the crawler and auth section classes.
"""
2021-05-05 23:45:10 +02:00
def __init__(self, section: SectionProxy):
self.s = section
def error(self, key: str, desc: str) -> NoReturn:
raise ConfigOptionError(self.s.name, key, desc)
2021-05-05 23:45:10 +02:00
def invalid_value(
self,
key: str,
value: Any,
reason: Optional[str],
) -> NoReturn:
if reason is None:
self.error(key, f"Invalid value {value!r}")
else:
self.error(key, f"Invalid value {value!r}: {reason}")
2021-05-05 23:45:10 +02:00
def missing_value(self, key: str) -> NoReturn:
self.error(key, "Missing value")
class DefaultSection(Section):
def working_dir(self) -> Path:
pathstr = self.s.get("working_dir", ".")
return Path(pathstr).expanduser()
def explain(self) -> bool:
return self.s.getboolean("explain", fallback=False)
2021-05-23 22:41:59 +02:00
def status(self) -> bool:
return self.s.getboolean("status", fallback=True)
def report(self) -> bool:
return self.s.getboolean("report", fallback=True)
2021-05-24 13:10:19 +02:00
def share_cookies(self) -> bool:
return self.s.getboolean("share_cookies", fallback=True)
2021-04-27 12:41:49 +02:00
class Config:
@staticmethod
def _default_path() -> Path:
if os.name == "posix":
return Path("~/.config/PFERD/pferd.cfg").expanduser()
elif os.name == "nt":
return Path("~/AppData/Roaming/PFERD/pferd.cfg").expanduser()
else:
return Path("~/.pferd.cfg").expanduser()
2021-05-05 23:45:10 +02:00
def __init__(self, parser: ConfigParser):
2021-04-27 12:41:49 +02:00
self._parser = parser
self._default_section = DefaultSection(parser[parser.default_section])
@property
def default_section(self) -> DefaultSection:
return self._default_section
2021-04-27 12:41:49 +02:00
@staticmethod
2021-05-15 21:33:51 +02:00
def load_parser(parser: ConfigParser, path: Optional[Path] = None) -> None:
2021-04-27 12:41:49 +02:00
"""
May throw a ConfigLoadError.
2021-04-27 12:41:49 +02:00
"""
2021-05-19 18:10:17 +02:00
if path:
2021-05-24 13:17:28 +02:00
log.explain("Path specified on CLI")
2021-05-19 18:10:17 +02:00
else:
log.explain("Using default path")
2021-04-27 12:41:49 +02:00
path = Config._default_path()
log.explain(f"Loading {fmt_real_path(path)}")
2021-04-27 12:41:49 +02:00
# Using config.read_file instead of config.read because config.read
# would just ignore a missing file and carry on.
try:
with open(path) as f:
parser.read_file(f, source=str(path))
except FileNotFoundError:
raise ConfigLoadError(path, "File does not exist")
2021-04-27 12:41:49 +02:00
except IsADirectoryError:
raise ConfigLoadError(path, "That's a directory, not a file")
2021-04-27 12:41:49 +02:00
except PermissionError:
raise ConfigLoadError(path, "Insufficient permissions")
2021-04-27 12:41:49 +02:00
def dump(self, path: Optional[Path] = None) -> None:
"""
May throw a ConfigDumpError.
2021-04-27 12:41:49 +02:00
"""
2021-05-23 11:04:50 +02:00
if path:
log.explain("Using custom path")
else:
log.explain("Using default path")
2021-04-27 12:41:49 +02:00
path = self._default_path()
log.explain(f"Dumping to {fmt_real_path(path)}")
log.print(f"[bold bright_cyan]Dumping[/] to {escape(fmt_real_path(path))}")
2021-04-27 12:41:49 +02:00
try:
path.parent.mkdir(parents=True, exist_ok=True)
except PermissionError:
raise ConfigDumpError(path, "Could not create parent directory")
2021-04-27 12:41:49 +02:00
try:
# Ensuring we don't accidentally overwrite any existing files by
# always asking before overwriting a file.
try:
# x = open for exclusive creation, failing if the file already
# exists
with open(path, "x") as f:
self._parser.write(f)
except FileExistsError:
print("That file already exists.")
2021-05-15 21:33:51 +02:00
if asyncio.run(prompt_yes_no("Overwrite it?", default=False)):
2021-04-27 12:41:49 +02:00
with open(path, "w") as f:
self._parser.write(f)
else:
raise ConfigDumpError(path, "File already exists")
2021-04-27 12:41:49 +02:00
except IsADirectoryError:
raise ConfigDumpError(path, "That's a directory, not a file")
2021-04-27 12:41:49 +02:00
except PermissionError:
raise ConfigDumpError(path, "Insufficient permissions")
2021-04-30 16:22:14 +02:00
2021-05-15 21:33:51 +02:00
def dump_to_stdout(self) -> None:
self._parser.write(sys.stdout)
2021-05-25 15:49:06 +02:00
def crawl_sections(self) -> List[Tuple[str, SectionProxy]]:
2021-04-30 16:22:14 +02:00
result = []
for name, proxy in self._parser.items():
if name.startswith("crawl:"):
result.append((name, proxy))
2021-04-30 16:22:14 +02:00
return result
2021-05-25 15:49:06 +02:00
def auth_sections(self) -> List[Tuple[str, SectionProxy]]:
result = []
for name, proxy in self._parser.items():
if name.startswith("auth:"):
result.append((name, proxy))
return result