Tuesday, June 27, 2017

Python argparse with defaults -- in-script and importable

I've tried using argparse (and optparse before that) and have had trouble finding a workflow that made me happy. This one is pretty close. It allows you to make defaults for when you call a script on the command line and makes those same defaults available if you import the module. It works like this:
  1. Make a dictionary with the defaults you want.
  2. In the function you write that parses the arguments, use the dictionary values as the defaults.
  3. Merge the defaults dictionary and the arguments dictionary together.
  4. Return the merged dictionary.
The Gist version is available here, or you can see it below, too.

 #######################################

#!/usr/bin/python3
'''
> python3 args_example.py -h
usage: args_example.py [-h] [-f FIRST_OPTION] [-s SECOND_OPTION]
                       mandatory_argument

Example argument parser with default values.

positional arguments:
  mandatory_argument    Not everything can have a default value. This is a
                        mandatory argument.

optional arguments:
  -h, --help            show this help message and exit
  -f FIRST_OPTION, --first_option FIRST_OPTION
                        This is the first option. Default:
                        first_default_value.
  -s SECOND_OPTION, --second_option SECOND_OPTION
                        This is the second option. (Default:
                        second_default_value)
  -t THIRD_OPTION, --third_option THIRD_OPTION
                        This is the third option. (Default:
                        third_default_value)

> python3 args_example.py -f "non-default first value" --second_option 'non-default second value' "this is mandatory"                                                      
{'first_option': 'non-default first value',
 'mandatory_argument': 'this is mandatory',
 'second_option': 'non-default second value',
 'third_option': 'third_default_value'}

Of course, https://pymotw.com/2/argparse/ is still excellent.
'''


import argparse
import pprint


default_settings = {'first_option': 'first_default_value',
                    'second_option': 'second_default_value',
                    'third_option': 'third_default_value'}


def get_args(settings=default_settings):
    '''Reads from sys.stdin. Use stdin to get new options, and return options
    merged with the default options. Settings is a dictionary. We'll have an
    argument (no, you came here for an argument), set two options, and default
    one.
    '''

    parser = argparse.ArgumentParser(description='Example argument parser with default values.')
    parser.add_argument('mandatory_argument', help='Not everything can have a default value. This is a mandatory argument.')
    parser.add_argument('-f', '--first_option', help='This is the first option. Default: %(default)s.',
                        default=settings['first_option'])
    parser.add_argument('-s', '--second_option', help='This is the second option. (Default: %(default)s)',
                        default=settings['second_option'])
    parser.add_argument('-t', '--third_option', help='This is the third option. (Default: %(default)s)',
                        default=settings['third_option'])
    args = parser.parse_args()
    args_settings = vars(args) # everything is easier with a dictionary
 
    #python3.5+
    merged_settings_3 = {**settings, **args_settings}

    #python2
    merged_settings_2 = default_settings.copy()
    merged_settings_2.update(args_settings)

    merged_settings_brute_force = '''
    if args.first_option:
        settings['first_option'] = args.first_option
    if args.second_option:
        settings['second_option'] = args.second_option
    if args.third_option:
        settings['third_option'] = args.third_option
    settings['mandatory_argument'] = args.mandatory_argument
    return merged_settings_brute_force
        '''

    return merged_settings_3


def main():
    settings = get_args()
    pprint.pprint(settings)


if __name__ == '__main__':
    main()

Saturday, June 10, 2017

Cat facts & python

I'd not actually used anything with json before, but (unsurprisingly) the requests library came through.

>>> import requests
>>> url = 'http://catfacts-api.appspot.com/api/facts?number=1'
>>> print(requests.get(url).json()['facts'][0])
Cat families usually play best in even numbers. Cats and kittens should be acquired in pairs whenever possible.

While that gets you the answer, when did you ever really just want the answer? That's pretty uninteresting. Let's get there in stages.

This is straightforward (yes, I'm assuming you have requests installed already):

>>> import requests
>>> url = 'http://catfacts-api.appspot.com/api/facts?number=1'

Then we can get the cat fact:
 >>> cat_fact = requests.get(url)

Which looks like this:
>>> cat_fact.text
'{"facts": ["A cat\'s whiskers are thought to be a kind of radar, which helps a cat gauge the space it intends to walk through."], "success": "true"}'

Well, we really just want the string of the dictionary item in the string. The json() method makes it a dictionary, not a string.
 >>> cat_fact.json()
{'facts': ["A cat's whiskers are thought to be a kind of radar, which helps a cat gauge the space it intends to walk through."], 'success': 'true'}





And we can use the dictionary key ('facts')...
>>> print(cat_fact.json()['facts'])
["A cat's whiskers are thought to be a kind of radar, which helps a cat gauge the space it intends to walk through."]

But we don't want a list, we want the string. So take the first item in the list.
>>> print(cat_fact.json()['facts'][0])
A cat's whiskers are thought to be a kind of radar, which helps a cat gauge the space it intends to walk through.

We could also iterate through a number of cat facts, including a spurious use of f-string formatting.
>>> number = 3
>>> url_number = f'http://catfacts-api.appspot.com/api/facts?number={number}'
>>> catfact = requests.get(url_number).json()
>>> catfact
{'facts': ['Not every cat gets "high" from catnip. Whether or not a cat responds to it depends upon a recessive gene: no gene, no joy.', 'Cats have an average of 24 whiskers, arranged in four horizontal rows on each side.', 'The strongest climber among the big cats, a leopard can carry prey twice its weight up a tree.'], 'success': 'true'}

>>> for fact in catfact['facts']:
...   print(fact)
...
Not every cat gets "high" from catnip. Whether or not a cat responds to it depends upon a recessive gene: no gene, no joy.
Cats have an average of 24 whiskers, arranged in four horizontal rows on each side.
The strongest climber among the big cats, a leopard can carry prey twice its weight up a tree.

Which I think is quite enough about cat facts.