Rework cli to accept ranges

This commit is contained in:
Alex Selimov 2025-04-21 22:03:32 -04:00
parent 77ec5ffff1
commit 32007d5c36
2 changed files with 192 additions and 17 deletions

View File

@ -2,7 +2,7 @@ import argparse
import sys import sys
from pathlib import Path from pathlib import Path
from .maildir import parse_maildir from .maildir import MailDir, TopSender, parse_maildir
def parse_arguments() -> argparse.Namespace: def parse_arguments() -> argparse.Namespace:
@ -43,30 +43,118 @@ def cli():
if args.verbose: if args.verbose:
print(f"Analyzing emails in {maildir_path}...") print(f"Analyzing emails in {maildir_path}...")
run_loop(args, maildir_path)
return 0
def run_loop(args: argparse.Namespace, maildir_path: str | Path):
# Set up maildir
maildir = parse_maildir(maildir_path) maildir = parse_maildir(maildir_path)
if args.verbose: if args.verbose:
print(f"Found {len(maildir._df)} emails") print(f"Found {len(maildir._df)} emails")
top_senders = maildir.get_top_n_senders(args.top) # Main running loop
while True:
top_senders = maildir.get_top_n_senders(args.top)
if not top_senders: if not top_senders:
print("No senders found in the maildir", file=sys.stderr) print("No senders found in the maildir", file=sys.stderr)
return 0 return 0
result = [] result = []
for i, sender in enumerate(top_senders, 1): for i, sender in enumerate(top_senders, 1):
names_str = ", ".join(sender.names[:5]) # Limit to first 5 names names_str = ", ".join(sender.names[:5]) # Limit to first 5 names
if len(sender.names) > 5: if len(sender.names) > 5:
names_str += f" and {len(sender.names) - 5} more" names_str += f" and {len(sender.names) - 5} more"
result.append( result.append(
f"{i}. {sender.email} - Email count: {sender.count} - Names used: {names_str}" f"{i}. {sender.email} - Email count: {sender.count} - Names used: {names_str}"
)
output = "\n".join(
[f"Top {len(top_senders)} senders in {maildir_path}:", "=" * 40, *result]
) )
output = "\n".join( print(output)
[f"Top {len(top_senders)} senders in {maildir_path}:", "=" * 40, *result] if not user_input_loop(top_senders, maildir):
) break
print(output)
return 0 def user_input_loop(top_senders: list[TopSender], maildir: MailDir) -> bool:
user_input = input("> ").strip()
while handle_user_input(user_input, top_senders, maildir):
user_input = input("> ").strip()
def parse_selections(user_input, max_selection):
"""
Parse user input into a set of valid selections.
Args:
user_input (str): User input string with numbers, comma-separated lists, or ranges
max_selection (int): Maximum allowed selection number
Returns:
set: Set of valid selection numbers
Raises:
ValueError: If any input is invalid or out of range
"""
selections = set()
# Clean up the input
user_input = user_input.strip()
# Split by comma
items = [item.strip() for item in user_input.split(",")]
for item in items:
if "-" in item:
# Handle range (e.g., "1-3" or "4 - 5")
range_parts = [part.strip() for part in item.split("-")]
if (
len(range_parts) != 2
or not range_parts[0].isdigit()
or not range_parts[1].isdigit()
):
raise ValueError(f"Invalid range format: {item}")
start = int(range_parts[0])
end = int(range_parts[1])
if start > end:
raise ValueError(
f"Invalid range: {item}. Start must be less than or equal to end."
)
selections.update(range(start, end + 1))
elif item.isdigit():
# Handle single number
selections.add(int(item))
else:
raise ValueError(f"Invalid input: {item}")
# Check if any selection is out of range
out_of_range = [s for s in selections if s < 1 or s > max_selection]
if out_of_range:
raise ValueError(
f"Selection(s) out of range: {', '.join(map(str, out_of_range))}. Valid range is 1-{max_selection}"
)
return selections
def handle_user_input(user_input, top_senders, maildir):
if user_input.lower() == "q":
return False
try:
selections = parse_selections(user_input, len(top_senders))
for selection in selections:
selected_sender = top_senders[selection - 1]
print(f"Selected {selected_sender.email}")
return True
except ValueError:
print("Please enter a valid number or 'q' to quit")

87
tests/test_cli.py Normal file
View File

@ -0,0 +1,87 @@
import pytest
from pathlib import Path
# Import the function to test - assuming it's in a module called 'maildir_analyzer'
# Update this import to match your actual module structure
from maildirclean.cli import parse_selections
def test_single_number():
result = parse_selections("3", 5)
assert result == {3}
def test_comma_separated_list():
result = parse_selections("1,3,5", 5)
assert result == {1, 3, 5}
def test_range():
result = parse_selections("2-4", 5)
assert result == {2, 3, 4}
def test_range_with_spaces():
result = parse_selections("1 - 3", 5)
assert result == {1, 2, 3}
def test_combined_input():
result = parse_selections("1,3-5,7", 10)
assert result == {1, 3, 4, 5, 7}
def test_duplicate_numbers():
result = parse_selections("1,1,2,2-4,3", 5)
assert result == {1, 2, 3, 4} # Set removes duplicates
def test_invalid_format():
with pytest.raises(ValueError, match="Invalid input: abc"):
parse_selections("abc", 5)
def test_invalid_range_format():
with pytest.raises(ValueError, match="Invalid range format: 2-a"):
parse_selections("2-a", 5)
def test_inverted_range():
with pytest.raises(ValueError, match="Invalid range: 5-2"):
parse_selections("5-2", 5)
def test_out_of_range():
with pytest.raises(ValueError, match="Selection.*out of range"):
parse_selections("3,6,8", 5)
def test_mix_valid_and_invalid():
with pytest.raises(ValueError):
parse_selections("1,abc,3", 5)
def test_empty_input():
with pytest.raises(ValueError):
parse_selections("", 5)
def test_whitespace_only():
with pytest.raises(ValueError):
parse_selections(" ", 5)
def test_max_boundary():
# Test the boundary case
result = parse_selections("5", 5)
assert result == {5}
def test_complex_input():
result = parse_selections("1-2, 4, 6-8", 10)
assert result == {1, 2, 4, 6, 7, 8}
def test_negative_numbers():
with pytest.raises(ValueError, match="Invalid range format:*"):
parse_selections("-1,2", 5)