Capstone: A Persistent Lending Tool · practice
Initialize the Catalog Explicitly
A missing catalog file should be created only because somebody asked for init. If available contains a typo, or --help was requested, the database path must remain untouched. lending.py now needs its first dispatch branch and its first command
Run will make this exact call with a path inside a temporary directory:
database_path = temporary_directory / "catalog.db"
main(["init", str(database_path)])
After argparse succeeds, the namespace contains args.command == "init" and args.database == str(database_path). Add setup_error_is_documented(error) exactly as shown, then add init_command(args) and the explicit args.command == "init" branch in main:
def setup_error_is_documented(error):
message = str(error)
if message == "Database has tables but no schema version.":
return True
if message.startswith("Schema version "):
if message.endswith(" is not supported."):
return True
if message.endswith(" does not match the expected tables and columns."):
return True
return False
if args.command == "init":
return init_command(args)
Keep parsing above the branch. This order is what prevents help and parser errors from reaching database setup.
Inside init_command, pass args.database to prepare_database. That one call already owns the full compatibility matrix: it creates a missing or blank version 2 database, migrates an exact version 1 database, leaves exact version 2 unchanged, and refuses every unsupported or marker-shape disagreement without changing it. Do not copy that logic into lending.py.
Only a setup_error_is_documented is an expected application refusal here. Catch ValueError, re-raise it when the function returns False, and otherwise pass it to report_error for one stderr line and status 1. This keeps an unrelated ValueError, OperationalError, ProgrammingError, or TypeError visible with its original type. Printing success before prepare_database returns would lie when setup later fails, so print only afterward:
Database ready (schema version 2).
Return 0 after the line is written. The function does not open another connection merely to inspect the version. prepare_database has already verified the result and closed the setup connection it owns.
Press Run. demo.py calls main(["init", str(database_path)]), captures the returned status and both streams, then opens the temporary file only long enough to read its stored marker:
== init ==
Status: 0
stdout: Database ready (schema version 2).
stderr: <empty>
Schema version after reopening: 2
A thorough check repeats the call with missing, exact version 1, exact version 2, and incompatible databases. It also injects an unrelated database failure. Expected compatibility messages become status 1; the unrelated failure must still surface for repair.
Task
Add the exact setup_error_is_documented(error) init_command(args), and add this branch in main:
if args.command == "init":
return init_command(args)
Call prepare_database(args.database). On success, print Database ready (schema version 2). and return 0. Report only a