0%

Chapter 11 · practice

Capstone: A Persistent Lending Tool

Choose a Command Before Opening the Database

The lending program needs seven operations, but a mistyped command must not create or open a database by accident. That makes selection the first boundary in lending.py. main(argv) must understand the request completely before any database runs.

Here is the project you can see:

PathRole
lending.pyThe file you edit. Its main(argv) function is the entry point and the function Run calls.
catalog_db.pyOpens configured connections and provides the lending commands and queries.
catalog_setup.pyCreates, checks, and migrates the catalog schema.
catalog_import.pyReads, validates, and imports item CSV data.
data/items.csvThree sample items used by later commands.
tests/test_catalog.pyThe existing eight-test database suite.
demo.pyThe visible Run entry. It calls main(["--help"]) for this first step.

The three catalog_*.py files are already complete. Keep your changes in lending.py and call their named functions.

If you use Reset project later, return to the page you are working on first. Reset restores that page’s complete starting lending.py.

Open lending.py. Complete the two argument converters and build_parser(). The grammar is fixed:

init DATABASE
import-items DATABASE CSV
register-member DATABASE ID MEMBER_CODE NAME EMAIL [--loan-limit NUMBER]
checkout DATABASE LOAN_ID ITEM_ID MEMBER_ID CHECKED_OUT_ON
return DATABASE LOAN_ID RETURNED_ON
available DATABASE
activity DATABASE

A subcommand selects a parser for one operation. The parent parser reads the command name; that command’s parser reads only its own arguments. Start with the smallest case:

commands = parser.add_subparsers(dest="command", required=True)
init_parser = commands.add_parser("init")
init_parser.add_argument("database")

required=True makes a missing command an error. dest="command" stores the chosen name in args.command. Parsing ["init", "catalog data.db"] produces args.command == "init" and args.database == "catalog data.db"; parsing does not open that path.

Repeat commands.add_parser(...) for each other command, then add that command’s arguments to its returned parser. For example, registration needs this ID argument:

register_parser = commands.add_parser("register-member")
register_parser.add_argument("member_id", metavar="ID", type=positive_integer)

The first names the Python attribute, args.member_id. metavar="ID" only labels it in help. Use lowercase versions of the grammar’s other argument names, such as loan_id and checked_out_on, for their attributes. Each command needs its own database argument. The optional --loan-limit becomes args.loan_limit; give it type=positive_number and default=3.

A converter receives one argument as text. It returns the converted or raises argparse.ArgumentTypeError with a useful message. For the numeric converter, catch a failed float(value) conversion and reject values for which math.isfinite(number) is false or number <= 0. math.isfinite rejects infinity and nan, which do not make usable loan limits. The integer converter can require value.isascii() and value.isdigit() before converting and checking that the result is positive.

Use required subcommands. Parse every ID as a positive base-10 integer. Parse an explicit --loan-limit as a positive finite float; its default is the integer 3. Values such as 0, -2, nan, and inf are parser errors. The text fields remain strings for now. Their command functions will trim them as more commands are added, where an empty value can receive a precise application message.

main(argv=None) must parse the supplied . When argv is None, it uses sys.argv[1:]. Do not catch argparse’s SystemExit: help must exit with status 0, while a missing command, unknown command, missing argument, or malformed typed value exits with status 2. Those outcomes happen before dispatch, so they cannot become an application status of 1.

Press Run. demo.py captures the help call, reports its status, and counts database-like files in an empty temporary directory:

== choose a command ==
Help status: 0
Commands: init, import-items, register-member, checkout, return, available, activity
Database files created: 0

No command function exists yet, so Run stops there. A thorough parser check also tries the complete grammar with unseen values, including a path containing spaces and Unicode. It checks parsing only and never asks an unfinished command to touch the database.

Task

Complete positive_integer, positive_number, and build_parser in lending.py for the exact init, import-items, register-member, checkout, , available, and activity grammar.

Make the subcommand required. Use the converters for every ID and for --loan-limit, whose default is integer 3. Keep main(argv=None) import-safe, let argparse’s help and parser failures raise their original SystemExit, and do not open a database during parsing.