Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 101 additions & 15 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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

Expand Down
31 changes: 0 additions & 31 deletions convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 14 additions & 5 deletions form.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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}")
Expand Down