6
\$\begingroup\$

I'm currently working on a command-line "database migration" utility. Here are some of the requirements regarding reading the command-line arguments part:

  • It should accept regular database connection parameters: host, database name, port, user and password (do not allow to specify password as a command-line argument, ask for password separately)
  • It works in two modes - "validate" and "apply" - one of them has to be specified explicitly, both cannot be specified
  • There should be an integer "batch size" argument specified
  • There can be an optional range of integer ids specified via --id-begin and --id-end (the beginning of the range defaults to 1 if not specified)
  • Instead of a range, there can also be a --name argument specified
  • Both name and id range cannot be present
  • There should be a "verbose" flag

My Solution

Here is what I came up with:

I'm using argparse module and extracted the parsing command-line arguments into a separate .parse_args() function which returns the parsed arguments (which is argparse.Namespace instance):

import argparse
import getpass


def parse_arguments(args):
    """Parses command-line arguments."""
    parser = argparse.ArgumentParser()

    # general database connection settings
    parser.add_argument('-H', '--host', help="Database host IP address", required=True)
    parser.add_argument('-D', '--database', help="Database name", required=True)
    parser.add_argument('-P', '--port', help="Database port", required=True, type=int)

    parser.add_argument('-u', '--user', help='Database user name', required=True)

    # modes
    parser.add_argument('--validate', help='Enables "validate only" mode', action='store_true')
    parser.add_argument('--apply', help='Enables "apply" mode', action='store_true')

    # extra settings
    parser.add_argument('-b', '--batch-size', help='Batch size - determines how many cases are processed at a time',
                        type=int, default=5000)
    parser.add_argument('--id-begin', help='ID for the processing to begin with', type=int, default=None)
    parser.add_argument('--id-end', help='ID for the processing to end with', type=int, default=None)
    parser.add_argument('--name', help='NAME value', type=str, default=None)

    parser.add_argument('--verbose', help='Enables "verbose" reporting mode', action='store_true', default=False)

    args = parser.parse_args(args)

    # if both specified, id end cannot be smaller than id begin
    if args.id_begin and args.id_end and args.id_end < args.id_begin:
        raise parser.error("Invalid ID range.")

    # at least one mode should be specified
    if not args.validate and not args.apply:
        raise parser.error('Please specify one of the "validate" or "apply" modes.')

    # don't allow to use both "apply" and "validate" mode
    if args.apply and args.validate:
        raise parser.error('Cannot use both "validate" and "apply" modes.')

    # don't allow to use both NAME and ID
    if args.name and (args.id_begin or args.id_end):
        raise parser.error("Cannot use both NAME and ID.")

    # set the default id_begin value
    if not args.name and not args.id_begin:
        args.id_begin = 1

    args.password = getpass.getpass(prompt='Enter password: ')

    return args

The Questions

The code works and I've even covered it with tests (put them into a gist). But, I don't particularly like the way I check the requirements for name and id ranges, mutually exclusive "apply" and "validate" modes. It feels like I could have used more argparse specific features like custom types or actions. What do you think?

I would also appreciate other ideas about handling the password - using getpass() helps to not have the password shown on the terminal explicitly, but requires mocking it in tests.

\$\endgroup\$
4
+100
\$\begingroup\$

Your function should have args=None as default. This way you can use it to parse the command-line arguments as well as pass in a list of arguments to parse.

For your mutual exclusive options you should use...mutual exclusive groups. However, this only works properly for your mode, because adding a group of not mutually exclusive options (your --id-begin and --id-end) to a mutually exclusive group seems to no be supported at the moment, even though there was some activity on that a few years back: http://bugs.python.org/issue10984

# modes
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument(
    '--validate', help='Enables "validate only" mode', action='store_true')
action.add_argument(
    '--apply', help='Enables "apply" mode', action='store_true')

With the above code, you can remove all checking you currently do on the modes. It requires that exactly one of the modes is activated.

Also note that the default behavior of action='store_true' is to have default=False, so you can just do:

parser.add_argument('--verbose', help='Enables "verbose" reporting mode',
                    action='store_true')
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.