diff --git a/config.py b/config.py index 295768c..1f73432 100644 --- a/config.py +++ b/config.py @@ -3,12 +3,13 @@ # This module holds config related functions. This includes the EntryInfo data # class too. -from collections import namedtuple -from dataclasses import dataclass +from __future__ import annotations -from utils import to_form_url +import dataclasses -@dataclass +from utils import to_form_url, to_normal_form_url + +@dataclasses.dataclass class EntryInfo: required: bool prompt: bool @@ -47,16 +48,19 @@ def from_string(cls, string): """ string = string.strip() + # Check if the entry is required if not string: raise ValueError("Empty entry") required = (string[0] == "*") string = string.removeprefix("*").strip() + # Check if the entry will use a prompt if not string: raise ValueError("Missing type") prompt = (string[0] == "!") string = string.removeprefix("!").strip() + # Get the entry type type, split, string = map(str.strip, string.partition("-")) for name, aliases in cls.TYPES.items(): if type == name: @@ -69,12 +73,14 @@ def from_string(cls, string): if not split: raise ValueError("Missing type-key split '-'") + # Get the entry key key, split, string = map(str.strip, string.partition(";")) if not key: raise ValueError("Missing key") if not split: raise ValueError("Missing key-title split ';'") + # Get the entry title / value title, split, value = map(str.strip, string.partition("=")) if not title: title = key # Title defaults to the key if absent. @@ -89,24 +95,104 @@ def __str__(self): f"-{self.key};{self.title}={self.value}" ) -ConfigInfo = namedtuple("ConfigInfo", "url entries") -def open_config(file): - """ - Open config file and return the URL and entries. - """ - if isinstance(file, str): - file = open(file) - with file: +@dataclasses.dataclass +class ConfigInfo: + url: str + entries: list[EntryInfo] + title: Optional[str] + description: Optional[str] + + @classmethod + def from_file(cls, file): + """ + Open config file and return the URL and entries. + """ + if isinstance(file, str): + with open(file) as file: + return cls.from_file(file) + url = to_form_url(file.readline()) entries = [] - for line in file: - line = line.strip() + title = None + description = [] + + fileiter = map(str.strip, iter(file)) + for line in fileiter: + + # Special code to detect autogenerated title / description + if line.startswith("#"): + if title is not None: # Already defined + continue + if entries: # Title / description are before entries + continue + search = {"auto-generated", "info"} + if not any(word in line.lower() for word in search): + continue + try: + if (line := next(fileiter)).startswith("# "): + title = line.removeprefix("# ") + while (line := next(fileiter)).startswith("# "): + description.append(line.removeprefix("# ")) + except StopIteration: + break + else: + pass # Fall-through to other options + + # Normal processing if not line: continue if line.startswith("#"): + # This is checked again because there could be another comment + # after the description. continue + + # Parse and add to the entries list entries.append(EntryInfo.from_string(line)) - return ConfigInfo(url, entries) + + # Make description an Optional[str] + if not description: + description = None + else: + description = "\n".join(description) + + return cls(url, entries, title, description) + + # Create entries from info + # `info` needs "types", "titles", "keys", "required", and "options" + @classmethod + def from_info(cls, info): + entries = [] + if info["takes_email"]: + args = (True, True, "extra", "emailAddress", "Email address", "") + entries.append(EntryInfo(*args)) + for type, title, key, required, options in zip( + info["types"], info["titles"], info["keys"], + info["required"], info["options"], + ): + if options: + title = f"{title} ({', '.join(options)})" + entries.append(EntryInfo(required, True, type, key, title, "")) + return cls( + info["form_url"], entries, + info.get("form_title"), info.get("form_description") + ) + + # Iterator of config lines from info + def to_config_lines(self): + # First line should be a link that you can paste into a browser + yield to_normal_form_url(self.url) + + # Note that the file was auto-generated + yield "# Auto-generated using form.py" + + if self.title: + yield f"# {self.title}" + if self.description: + for line in self.description.splitlines(): + yield f"# {line}" + + for entry in self.entries: + yield str(entry) # - Tests diff --git a/convert.py b/convert.py index ce4cb3e..00240fe 100644 --- a/convert.py +++ b/convert.py @@ -107,37 +107,6 @@ def get_options(question): def form_info(soup): return info_using_soup(soup) | info_using_json(form_json_data(soup)) -# Create entries from info -# `info` needs "types", "titles", "keys", "required", and "options" -def entries_from_info(info): - entries = [] - if info["takes_email"]: - args = (True, True, "extra", "emailAddress", "Email address", "") - entries.append(EntryInfo(*args)) - for type, title, key, required, options in zip( - info["types"], info["titles"], info["keys"], - info["required"], info["options"], - ): - if options: - title = f"{title} ({', '.join(options)})" - entries.append(EntryInfo(required, True, type, key, title, "")) - return entries - -# Iterator of config lines from info -def config_lines_from_info(info): - # First line should be a link that you can paste into a browser - yield to_normal_form_url(info["form_url"]) - - # Note that the file was auto-generated - yield f"# Auto-generated using form.py" - - yield f"# {info['form_title']}" - for line in info["form_description"].splitlines(): - yield f"# {line}" - - for entry in entries_from_info(info): - yield str(entry) - # - Tests # Test that the info from soup and from json match diff --git a/form.py b/form.py index 1d71eed..7bb7363 100644 --- a/form.py +++ b/form.py @@ -5,8 +5,8 @@ from configparser import Error as ConfigError from contextlib import suppress -from config import open_config -from convert import form_info, config_lines_from_info +from config import ConfigInfo +from convert import form_info from process import prompt_entry, parse_entries, format_entries from utils import to_form_url, url_from_shortcut @@ -121,9 +121,17 @@ def process(target="config.txt", *, command_line=False, should_submit=None): # Read and process the file with file: print_("Reading config entries...") - config = open_config(file) - print_(f"Form URL: {config.url}") + config = ConfigInfo.from_file(file) + # Print out config info + print_(f"URL: {config.url}") + if config.title: + print_(config.title) + if config.description: + for line in config.description.splitlines(): + print_(f" {line}") + + # Parse and format entries messages = parse_entries(config.entries, on_prompt=prompt_entry) data = format_entries(config.entries, messages) print_(f"Form data: {data}") @@ -204,11 +212,12 @@ def convert( print_("Converting form...") soup = BeautifulSoup(text, "html.parser") info = form_info(soup) + config = ConfigInfo.from_info(info) # Write the info to the config file print_(f"Writing to config file: {target}") with open(target, mode="w") as file: - for line in config_lines_from_info(info): + for line in config.to_config_lines(): file.write(line + "\n") print_(f"Form converted and written to file: {target}")