diff --git a/form.py b/form.py index 279f02e..7d58551 100644 --- a/form.py +++ b/form.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from string import ascii_letters, digits -from process import prompt_entry, parse_entries, format_entries +from process import prompt_and_parse_entries, format_entries @dataclass class EntryInfo: @@ -421,7 +421,7 @@ def process(target="config.txt", *, command_line=False, should_submit=None): config = open_config(file) print_(f"Form URL: {config.url}") - messages = parse_entries(config.entries, on_prompt=prompt_entry) + messages = prompt_and_parse_entries(config.entries) # Use default prompts data = format_entries(config.entries, messages) print_(f"Form data: {data}") diff --git a/process.py b/process.py index 7aab845..a47a417 100644 --- a/process.py +++ b/process.py @@ -1,11 +1,12 @@ # process.py # -# This module holds functions specifically for parsing config values and for -# returning formatted data dictionaries. +# This module holds functions for processing config files. This includes +# prompts, value parsers, and data formatters. from datetime import date, time, datetime # - Prompts +# entry -> value PROMPTS = { "words": "[Text]", @@ -16,29 +17,38 @@ "extra": "[Extra Data]", } -def prompt_entry(entry): +def prompt_entry(entry, *, prompts=PROMPTS): """ Prompt for a value to the passed entry. + + Print the entry's title and the appropriate hint (from the prompts dict). + If the user typed nothing, notify user to the default value (or that it + will be using an empty value). However, this message won't be printed if + there's no default value for a required question. """ - assert entry.prompt - while True: - value = input(f"{entry.title}: {PROMPTS[entry.type]} ").strip() - if not value: - if entry.required and not entry.value: - print(f"Value for entry '{entry.title}' is required") - continue - print(f"Using default value: {entry.value}") - value = entry.value - try: - return parse_value(value, entry.type) - except Exception as e: - if not entry.required and not value: - # If provided value isn't empty, it could be a mistake. Only - # skip when it is purposefully left empty. - return "" - print(type(e).__name__, *e.args) + value = input(f"{entry.title}: {prompts[entry.type]} ").strip() + if value: + return value + + if entry.value: + # Print this if there is a default value + print(f"Using default value: {entry.value}") + elif not entry.required: + # Print this if there's no default but its optional + print("Using empty value") + return entry.value + +def print_error(error, entry, value): + """ + Print the error and its reason. + """ + if entry.required and not value: + print(f"Value for entry '{entry.title}' is required") + else: + print(f"{type(error).__name__}: {', '.join(error.args)}") # - Parsers +# value -> message # Parsing functions (one str argument) def parse_normal(value): @@ -68,6 +78,7 @@ def parse_time(value): time(int(hour), int(minute)) # Check if time is real return [hour, minute] +# General function (takes a `type` argument) PARSERS = { "words": parse_normal, "choice": parse_normal, @@ -88,25 +99,90 @@ def parse_value(value, type): """ return PARSERS[type](value) -def parse_entries(entries, *, on_prompt=prompt_entry): +# Entry functions (uses entries) +def parse_entry(entry, value=None): + """ + Return the parsed value. + + Parse and return the message. If value is None, entry.value will be used. + If there's an error but the entry is optional and the value is empty, the + empty value will be returned. If the value isn't empty, the user could have + tried to enter a parsable value. + """ + if value is None: + value = entry.value + if value: + return parse_value(value, entry.type) + + if not entry.required: + # If provided value isn't empty, it could be a mistake. Only + # skip when it is purposefully left empty. + return "" + raise ValueError(f"Value for entry '{entry.title}' is required") + +def parse_entries(entries, values=None): + """ + Return the parsed values. + + Parse and return the messages using parsed_entry. If values is a list, pass + it as the second argument (value). + """ + if values is None: + return list(map(parse_entry, entries)) + else: + messages = [] + for entry, value in zip(entries, values): + messages.append(parse_entry(entry, value)) + return messages + +# - Prompt & Parse +# If you want to just parse entries without prompting, use parse_entries. These +# functions take an on_prompt and on_error for use in the command line. + +def prompt_and_parse_entry( + entry, *, + on_prompt=prompt_entry, + on_error=print_error, +): + """ + Prompt and return the parsed value. + + Prompt for an entry value and return the message. `on_prompt(entry)` will + be called to get a value. `on_error(exc, entry, value)` will be called if + parsing fails. This will loop until `parse_entry(entry, value)` returns + (without erroring). + """ + while True: + value = on_prompt(entry) # Not in try-except to prevent infinite loop + try: + return parse_entry(entry, value) + except Exception as exc: + on_error(exc, entry, value) + +def prompt_and_parse_entries( + entries, *, + on_prompt=prompt_entry, + on_error=print_error, +): """ Return a list of parsed messages. Parse the entries to create a list of messages. If the entry needs a - prompt, on_prompt is called with the entry. It should return a message or - raise an error. The result should be passed to `format_entries`. + prompt, `prompt_and_parse_entry(entry, **kwargs)` is used. Otherwise, + `parse_entry(entry)` is used. The result should be passed to + `format_entries`. """ messages = [] for entry in entries: if entry.prompt: - messages.append(on_prompt(entry)) - elif entry.required and not entry.value: - raise ValueError(f"Value for entry '{entry.title}' is required") + kwargs = dict(on_prompt=on_prompt, on_error=on_error) + messages.append(prompt_and_parse_entry(entry, **kwargs)) else: - messages.append(parse_value(entry.value, entry.type)) + messages.append(parse_entry(entry)) return messages # - Formatters +# key, message -> data # Specialized functions (key, message -> dict[str, str]) def format_normal(key, message): @@ -148,6 +224,15 @@ def format_message(key, type, message): """ return FORMATS[type](key, message) +# Entry functions (uses entries) +def format_entry(entry, message): + """ + Return a dictionary to be POSTed to the form. + + The rules for format_message apply here as well. + """ + return format_message(entry.key, entry.type, message) + def format_entries(entries, messages): """ Return a dictionary to be POSTed to the form. @@ -157,5 +242,5 @@ def format_entries(entries, messages): """ data = {} for entry, message in zip(entries, messages): - data |= format_message(entry.key, entry.type, message) + data |= format_entry(entry, message) return data