Creating a command line interface
In the previous lesson we saw how we could use functions to separate different tasks, and docstrings to describe the behaviour of these functions. In this lesson we’ll generalise our code even further by removing the global variables, adding functions parameters, and adding a command line interface.
The first thing that we will do is remove the global variables, and have these values passed to the functions directly. This will mean that someone reading the code for a function doesn’t have to hunt through the rest of the code to figure out what the global variables are.
For the generate_positions
function we can use parameters with the same name as the previously existing global variables, and we can set their default values to match. We have also taken the opportunity to update the docstring so that we know what types of parameters should be passed and what they are for.
def generate_positions(ref_ra='00:42:44.3',
ref_dec='41:16:09',
radius=1.,
nsources=1000):
"""
Create nsources random locations within radius of the reference position.
Parameters
----------
ref_ra, ref_dec : str
Reference position in "HH:MM:SS.S"/"DD:MM:SS.S" format.
Default position is Andromeda galaxy.
radius : float
The radius within which to generate positions. Default = 1.
nsources : int
The number of positions to generate
Returns
-------
ra, dec : numpy.array
Arrays of ra and dec coordinates in degrees.
"""
...
return
For the write_file
function, we already had two parameters that needed to be passed, but now we’ll add another which is the output file name. This wasn’t part of the global variables, but it was a hard coded file that we might want to change as we create multiple catalogs. Note that this function used to use the nsources
global variable to know how many ra/dec values were passed. In this revised version we just look at the length of the ras
list instead.
def write_file(ras, decs,
outfile='catalog.csv'):
"""
Write the ra/dec catalog to a file, and include a header and IDs.
Parameters
----------
ras, decs : list, numpy.array, or any iterable
Iterable of ra and dec coordinates. The length of these need to match.
outfile : str
Path/filename for the output file. (Overwite=True)
"""
with open(outfile, 'w') as f:
# creat a header row
print("id,ra,dec", file=f)
for i in range(len(ras)):
# use a csv format
print("{0}, {1:7.4f}, {2:7.4f}".format(i, ras[i], decs[i]), file=f)
return
By moving the global variables into the parameters of the functions, and making use of the default values we don’t need to make any further changes to our code. The following block is unchanged.
# Do the work
ras, decs = generate_positions()
write_file(ras, decs)
One advantage to our changes is that we could now generate a different catalog by adjusting these last two lines like this:
ras, decs = generate_positions(radius=2.)
write_file(ras,decs, outfile='catalog_2deg.csv')
We will leverage this ability in order to make a command line interface. The recommended way to do this is to use the argparse
module, and an if __name__
clause as follows.
if __name__ == '__main__':
# Set up the parser with all the options that you want
parser = argparse.ArgumentParser(prog='sim')
group1 = parser.add_argument_group()
group1.add_argument('--ref_ra', dest='ref_ra', type=str, default='00:42:44.3',
help='Central/reference RA position HH:MM:SS.S format')
group1.add_argument('--ref_dec', dest='ref_dec', type=str, default='41:16:09',
help='Central/reference Dec position DD:MM:SS.S format')
group1.add_argument('--radius', dest='radius', type=float, default=1.,
help='radius within which the new positions are generated (deg)')
group1.add_argument('--n', dest='nsources', type=int, default=1_000,
help='Number of positions to generate')
group1.add_argument('--out', dest='outfile', type=str, default='catalog.csv',
help='Filename for saving output (csv format)')
# parse the command line input
options = parser.parse_args()
Here we have created a single group of arguments called group1
, and then we add five different arguments to that group. Note that we provide both a call signature (--out
) for specifying a parameter, as well as the name/type/default value of that parameter (dest
/type
/default
). The help
is optional but highly recommended. If we were to run the code above with the --help
option then we would get the following output:
$> python sim.py --help
usage: sim [-h] [--ref_ra REF_RA] [--ref_dec REF_DEC] [--radius RADIUS] [--n NSOURCES] [--out OUTFILE]
optional arguments:
-h, --help show this help message and exit
--ref_ra REF_RA Central/reference RA position HH:MM:SS.S format
--ref_dec REF_DEC Central/reference Dec position DD:MM:SS.S format
--radius RADIUS radius within which the new positions are generated (deg)
--n NSOURCES Number of positions to generate
--out OUTFILE Filename for saving output (csv format)
Right away we have a way for people to understand how to use the program without having to open the source code. This will include you, two days from now, when you forgot some of the details of the code.
Finally, we can connect the user input to the program by using the options
object. Each of the parameters that were read in with a dest=thing
can be accessed using options.thing
. If we specified a type
then argparse will make sure that users don’t give input that can’t be converted to that type. The final part of our code now looks like this:
# parse the command line input
options = parser.parse_args()
ras, decs = generate_positions(ref_ra=options.ref_ra,
ref_dec=options.ref_dec,
radius=options.radius,
nsources=options.nsources)
write_file(ras, decs, outfile=options.outfile)
Since we specified default values for all of the inputs, we can run python sim.py
and it will run with the default values.