Smart and robust CLI input: validated prompts, menu-driven selections, default value support, and smart autocompletion.
Project description
AskUser
AskUser is a smart CLI utility for collecting and validating user input in Python. It wraps common prompt patterns—validation, menus, defaults, multi-selects, and autocompletion—into a simple, consistent API.
📑 Table of Contents
- Installation
- API Overview
validate_input- Menus & Options
- Database-Style Selection
- Yes/No Shortcut
- Autocomplete
- Validation Types
- Custom Validators (Extension API)
- Testing
- License
📦 Installation
pip install askuser
📖 API Overview
| Function / Class | What It Does |
|---|---|
validate_input(...) |
Prompt for free-form input, validate type/pattern, retry until valid |
pretty_menu(*args, **kwargs) |
Print a formatted menu |
validate_user_option(...) |
Show a menu and return the selected key |
validate_user_option_value(...) |
Return the selected value |
validate_user_option_enumerated(...) |
Enumerate a dict and return (key, value) |
validate_user_option_multi(...) |
Multi-select menu (returns keys) |
validate_user_option_value_multi(...) |
Multi-select menu (returns values) |
choose_from_db(...) |
Select an existing DB id from tabulated rows |
choose_dict_from_list_of_dicts(...) |
Select and return a dict |
yes(...) |
Yes/No shortcut |
user_prompt(...) |
Prompt with autocomplete |
SubstringCompleter |
Substring-based completer (advanced use) |
🔍 validate_input
validate_input(
input_msg: str,
validation_type: str | Literal[
'custom','required','not_in','none_if_blank','yes_no',
'int','float','decimal','alpha','alphanum','custom_chars','regex',
'date','future_date','time','url','slug','email','phone','language'
],
expected_inputs: list = None,
not_in: list = None,
maximum: int | float = None,
minimum: int | float = None,
allowed_chars: str = None,
allowed_regex: str = None,
default: Any = None
) -> Union[str,int,float,None]
Behavior
- Defaults
- If
defaultis set, pressing Enter returns the default.
- If
- Automatic hints
yes_no→(y/n)none_if_blank→(optional)time→(hh:mm:ss)maximum/minimum→(max: …)/(min: …)default→(default: …)
- Validation types
- Built-in:
int,float,decimal,alpha,alphanum,date,future_date,time,url,email,phone,slug,language. - List/pattern helpers:
customwithexpected_inputs=[...]not_inwithnot_in=[...]custom_charswithallowed_chars="abc123"regexwithallowed_regex="^[A-Z]+$"
- Built-in:
- Errors
- Invalid input raises internally and the user is re-prompted.
Example
count = validate_input(
"How many items?",
"int",
minimum=1,
maximum=100,
default=10
)
🧭 Menus & Options
pretty_menu(*args, **kwargs)
Prints a menu without prompting:
pretty_menu("List", "Add", d="Delete", q="Quit")
Output:
0: List 1: Add d: Delete q: Quit
Keys are case-sensitive. What you see is what you type.
validate_user_option(...)
validate_user_option(
input_msg: str = "Option:",
*args,
**kwargs # pass q=False to suppress quit
) -> Any
- Auto-adds
q: quitunlessq=False - Returns the selected key
- For
**kwargs - For
*args, keys are enumerated strings ("0","1", …)
opt = validate_user_option("Pick:", "Red", "Blue", g="Green")
# keys: '0','1','g','q'
opt = validate_user_option("Pick:", "One", "Two", q=False)
# keys: '0','1'
validate_user_option_value(...)
- Builds same menu, returns the value.
- No
qby default (legacy behavior).
genre = validate_user_option_value(a="Action", c="Comedy", d="Drama")
# 'c' → "Comedy"
validate_user_option_multi(...)
- Multi-select version of
validate_user_option. - Exit with
d: doneby default. Ifdis already used in your options, exit appears asxd, orxd2,xd3, ... - Pass
d=Falseto disable exit and force “pick until exhausted.” - Returns a list of keys in the order picked.
STATUS = {0: "new", 1: "active", 7: "rejected"}
picked = validate_user_option_multi("Select statuses:", **STATUS)
# → [1, 7]
- Exit with
d: done(orxd,xd2, … ifdis taken) - Pass
d=Falseto force selection until exhausted
validate_user_option_value_multi(...)
- Multi-select version of
validate_user_option_value.
vals = validate_user_option_value_multi(
"Pick genres",
a="Action",
c="Comedy",
d="Drama"
)
# user picks: c, a → ['Comedy', 'Action']
validate_user_option_enumerated(dict, msg="Option:", start=1)
validate_user_option_enumerated(
a_dict: dict,
msg: str = "Option:",
start: int = 1
) -> tuple
- Enumerates
.items()starting atstart. - Adds
q: quit. - Returns
(key, value)or('q', None).
movies = {101: "Inception", 202: "Memento"}
mid, title = validate_user_option_enumerated(movies, start=1)
- Enumerates dict items
- Adds
q: quit - Returns
(key, value)or('q', None)
🗄 Database-Style Selection
choose_from_db(db_result, input_msg=None, table_desc=None, xq=False)
choose_from_db(
db_result: list[dict],
input_msg: str = None,
table_desc: str = None,
xq: bool = False
) -> tuple
- Pretty-prints rows with
tabulate. - Only existing
idvalues indb_resultare valid. - If
xq=True, also acceptsxq→ returns('xq', 'quit'). - Invalid entries re-prompt.
choose_dict_from_list_of_dicts(list_of_dicts, key_to_choose)
choose_dict_from_list_of_dicts(
list_of_dicts: list[dict],
key_to_choose: str
) -> dict
- Menu of
dict[key_to_choose]. - Returns selected dict.
fruits = [
{"name": "Apple", "color": "red"},
{"name": "Banana", "color": "yellow"},
]
choice = choose_dict_from_list_of_dicts(fruits, "name")
✅ Yes/No Shortcut
yes("Continue?", default="y") # True if 'y', False if 'n'; blank → default
💬 Autocomplete
from askuser.autocomplete import user_prompt
country = user_prompt("Country: ", ["USA", "UK", "IN"])
# typing ≥2 chars will start showing suggestions from list
With dicts:
- return_value=True returns the value (defaults to False, returning the key)
opts = {"usa": "United States", "uk": "United Kingdom"}
val = user_prompt("Code: ", opts, return_value=True)
print(val)
# Code: usa
# United States
🔎 Validation Types
This table reflects actual runtime behavior, including case handling.
| Type | Description |
|---|---|
required |
Must not be blank |
none_if_blank |
Blank input returns None |
yes_no |
Accepts y / n (case-insensitive, normalized to lowercase) |
int |
Integer with optional minimum / maximum |
float |
Float with optional minimum / maximum |
decimal |
Decimal with optional bounds |
alpha |
Alphabetic characters only |
alphanum |
Alphanumeric characters only |
date |
YYYY-MM-DD or YYYY-MM-DD HH:MM:SS |
future_date |
Date must be today or in the future |
time |
HH:MM:SS |
email |
RFC-compliant email |
phone |
Digits with optional + (spaces/dashes stripped) |
url |
Hostname with optional path/query |
slug |
Lowercased [a-z0-9-], deduplicated delimiter |
language |
ISO-639 via pycountry |
custom |
Exact match against expected_inputs (case-sensitive) |
not_in |
Reject values in not_in (case-insensitive comparison) |
custom_chars |
Only characters in allowed_chars |
regex |
Must match provided regex |
Design note: Case-sensitivity is intentional.
If you want case-insensitive behavior forcustom, normalize input yourself or register a custom validator.
🧩 Custom Validators (Extension API)
AskUser supports registering additional validators at runtime without mutating internal globals directly.
from askuser import register_validator, validate_input
def is_even(user_input: str) -> int:
n = int(user_input)
if n % 2:
raise ValueError("Must be even")
return n
register_validator("even", is_even)
x = validate_input("Enter even number:", "even")
Helpers available:
get_validators()register_validator(name, func, overwrite=False)register_validators({name: func, ...}, overwrite=False)unregister_validator(name)
🧪 Testing
Under tests/, examples:
import pytest
from askuser import validate_input, yes, validate_user_option
def test_default(monkeypatch):
monkeypatch.setattr('builtins.input', lambda _: '')
assert validate_input("Prompt?", "int", default=7) == 7
def test_no_quit(monkeypatch):
monkeypatch.setattr('builtins.input', lambda _: '0')
assert validate_user_option("Pick:", "A", "B", q=False) == '0'
Run:
pytest tests/
📜 License
MIT — free to use, modify, and distribute.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file askuser-0.2.4.tar.gz.
File metadata
- Download URL: askuser-0.2.4.tar.gz
- Upload date:
- Size: 24.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fb3f307829dd506851272d8a4963d44cefb36c3ae4e7a5026fc196320ab3fca
|
|
| MD5 |
a2d635249e4f524778f18d96ddb95eae
|
|
| BLAKE2b-256 |
bb7e4a56ce5cc2cff593b90988bfeeb9945f34dfb5672e2070916ad609ec8ceb
|
File details
Details for the file askuser-0.2.4-py3-none-any.whl.
File metadata
- Download URL: askuser-0.2.4-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce64d0f05ac7c966b27b7eead4a5cd5b91ef3ecdc83aec04aeb13e50c53e2e14
|
|
| MD5 |
e151c2ee480e303729b4c5d6c63313c7
|
|
| BLAKE2b-256 |
8fd17121854b32f1e13e824de73a3de6e408c2a2d94c9fc1fc93685060896405
|