Refactoring script - working with argparse object

I looking for some help with refactoring a simple argparse script into functions. This is required so I can import the module for use with test cases.

The script works exactly as I want currently, with a default ‘rate’ value that can be overridden, and a --verbose option to print more detailed output to stdout.

I want to refactor the script into functions and have the get_options(args) function returning the argparse namespace object by return:

Namespace(rate=1.2, total=100.0, verbose=False)

But I am struggling to access the attributes that I require for my calculations (i.e. total and rate) and the verbose boolean value. Has anyone addressed similar problems?

I’ve looked into subparsing but I don’t think its for my use case. I know one of Zed’s video’s MorePy ex5 or 6 starts the pull up refactoring but it doesn’t complete.

A bit of a blunt force approach… but would this do what you want?

""" Simple tool for removing the amount of UK VAT applied to a total receipt.
    Over-enginneered to explore Python's ArgParse library in detail."""

def get_options():
    import argparse

    parser = argparse.ArgumentParser(
        prog='VAT Deductor',
        description='Displays the VAT deductable from a total receipt value')
    parser.add_argument(
        '--version',
        action='version',
        version='%(prog)s 1.0')
    parser.add_argument(
        '--rate',
        type=float,
        action='store',
        nargs='?',
        default='1.2',
        help='enter VAT rate as a decimal. Default is 1.2')
    parser.add_argument(
        '-v',
        '--verbose',
        action='store_true',
        help='displays Total Amount, VAT deductable & pretax values')
    parser.add_argument(
        'total',
        metavar='T',
        type=float,
        action='store',
        help='amount of TOTAL receipt value for processing')

    return parser.parse_args()


args = get_options()

net_value = round(args.total / args.rate, 2)
refund_vat = round(args.total - net_value, 2)

if args.verbose:
    print(f"""
Total Receipt:  £{args.total}
Net Value:      £{net_value}
Deductable VAT: £{refund_vat}
          """)
else:
    print(f"Deductable VAT: £{refund_vat}")

Or are you trying to build the parser dynamically from arguments that you would pass to get_options?

Sorry @florian I should have been more explicit. I am happy with the argparse function that returns the argparse object (namespace with attributes). What I want to do is then access the attributes in other functions so my calculation process can be tested in steps.

(Its a simple programme but I’ll be reusing this pattern in other programmes; using argparse to capture command line args for use elsewhere. The definition of main() is probably overkill but means I can hand craft the args in my tests to avoid mocking the user input).

e.g.

""" Simple tool for removing the amount of UK VAT applied to a total receipt.
    Over-engineered to explore Python's ArgParse library."""

import argparse
import sys


def get_options(args):
    """Creates and returns an argparse namespace object with attributes"""
    parser = argparse.ArgumentParser(
        prog='VATDeductor',
        description='Displays the VAT deductable from a total receipt value')
    parser.add_argument(
        '--version',
        action='version',
        version='%(prog)s 1.0')
    parser.add_argument(
        '--rate',
        type=float,
        action='store',
        nargs='?',
        default='1.2',
        help='enter VAT rate as a decimal. Default is 1.2')
    parser.add_argument(
        '--verbose',
        action='store_true',
        help='displays Total Amount, VAT deductable & pretax values')
    parser.add_argument(
        'total',
        metavar='T',
        type=float,
        action='store',
        help='amount of TOTAL receipt value for processing')
    return parser.parse_args(args)


def get_receipt_total():
    """Extracts the value from total variable and returns it."""
    pass


def calculate_net():
    """Calculates and returns the total receipt value divided by VAT rate, rounded to 2 decimal places."""
    pass


def calculate_refund():
    """Calculates and returns the total receipt value minus the calculated net value, rounded to 2 decimal places."""
    pass


def apply_verbosity():
    """If --verbose argument supplied, provides a verbose presentation of values to stdout, otherwise
    a default summary view is supplied."""
    pass


def main(args):
    get_options(args)

if __name__ == "__main__":
    main(sys.argv[1:])

What I can get my head around is how to access the returned get_options() object for the other functions…?

What if you make it global?

import argparse

parser = argparse.ArgumentParser()
parser.add_argument(
        "--rate",
        type=float,
        action="store",
        nargs="?",
        default="1.2",
        help="enter VAT rate as decimal.")

args = parser.parse_args()

def print_rate():
    global args
    print(args.rate)

print_rate()

Actually, global doesn’t even seem to be necessary for the function to access args in the outer scope.

Yeah that’s works fine but the parsing aspect of the code is not contained in a function.

And when it is it returns the argparse object not a variable reference. This is where I’m confused as if I want to pass the object as an argument to each function and/or access one or more of the attributes (like namespace.total).

EDIT:
I’m getting somewhere.

def get_receipt_total(namespace):
    """Extracts the value from total variable and returns it."""
    print(namespace[0])

As its a list object I can access it via the index. But…the attribute position is dynamic and will change if an overrider rate is applied. :frowning:

I’ll play some more.

FURTHER EDIT:
I think by converting the object to a dict is the easiest solution. I’ve also merged the calc again as that’s the bit I want tests for, not how argparse works. I think I’m getting there.

Example:

def get_options(args):
    """Creates and returns an argparse namespace object with attributes"""
    parser = argparse.ArgumentParser(
        prog='VATDeductor',
        description='Displays the VAT deductable from a total receipt value')
    parser.add_argument(
        '--version',
        action='version',
        version='%(prog)s 1.0')
    parser.add_argument(
        '--rate',
       [...]
    return vars(parser.parse_args())

# >>> {'rate': 1.2, 'verbose': True, 'total': 100.0}

def calculate_refund(namespace):
    """Calculates the total receipt value divided by VAT rate to get net, then total receipt
    minus the net, rounded to 2 decimal places. The returned value is the VAT deductable."""
    total_value = namespace['total']
    vat_rate = namespace['rate']
    net_value = round(total_value / vat_rate, 2)
    refund_value = round(total_value - net_value, 2)
    print(refund_value)


def main(args):
    calculate_refund((get_options(args)))
# >>> 16.67

if __name__ == "__main__":
    main(sys.argv[1:])

# >>> python vatd.py 100

Hehe, I think I’m slowly getting what you’re trying to achieve! :slight_smile:

So the problem with the global approach is that you would have to declare a global variable but you want everything contained in functions so you can test it better?

If you’re going for good modularity it might be better to pass the numeric values that calculate_refund needs explicitly. Then that function would be reusable in other contexts.

Something like

def calculate_refund(total, rate):
    ...

def main(argv):
    args = get_options(argv[1:])
    refund = calculate_refund(args.total, args.rate)
    ...

Or do your dictionary conversion and then calculate_refund(**args)?

Absolutely. I’ll implement the verbose function and then see how it sits with your suggestion. Thanks mate.

1 Like