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
4 changes: 2 additions & 2 deletions form.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}")

Expand Down
141 changes: 113 additions & 28 deletions process.py
Original file line number Diff line number Diff line change
@@ -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]",
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -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