As it stands, our package can still be run as a script, but the parameter values are still all hardcoded. In these exercises, we're going to add the ability to set the parameters through command line arguments and then with an input file.
The argparse
module makes reading command line arguments
pretty straightforward. There are three steps to using it:
First, create an ArgumentParser
:
import argparse
parser = argparse.ArgumentParser(
prog="ProgramName",
description="What the program does",
epilog="Text at the bottom of help",
)
Next, add arguments to the parser:
parser.add_argument("filename", help="Help text") # positional argument
parser.add_argument("-c", "--count") # option that takes a value
parser.add_argument("-v", "--verbose", action="store_true") # on/off flag
And then, finally, parse any command line arguments:
args = parser.parse_args()
The above is sufficient to get a simple command-line program running:
$ python3 example.py --help
usage: ProgramName [-h] [-c COUNT] [-v] filename
What the program does
positional arguments:
filename Help text
options:
-h, --help show this help message and exit
-c COUNT, --count COUNT
-v, --verbose
Text at the bottom of help
You can see that argparse
automatically added a --help
argument for us!
args
is an argparse.Namespace
object, and the
argument values can be accessed as a normal attribute:
>>> args.count
"2"
or we can convert it to a dict
using the builtin vars
:
>>> vars(args)
{'filename': 'some_file.txt', 'count': '2', 'verbose': True}
This can be particularly useful if the command line arguments have the same names as your function arguments, as we can use the dict-unpacking "double splat" operator:
do_some_maths(**vars(args))
although you may have to remove or add other arguments. As with everything in writing software, it depends!
Don't forget to run the tests!
- Create an
argparse.ArgumentParser
in yourmain
function - Add arguments for
A
,kappa
,delta
andR0
- Prefer using the keyword form
--argument-name
when your program takes multiple values, as multiple positional arguments are hard to read - The
type
argument toadd_argument
can be used to ensure the value is converted to the correct type - Make sure you add the
help
argument to each call
- Prefer using the keyword form
- Parse the arguments and pass them through to
flux_surface
- Try running your program manually and playing with the inputs
- We've still got a hardcoded filename -- let the user optionally set it
through a
--filename
argument, but have a sensible default.
Bonus:
- We always save the file and have to manually view it afterwards. Add a new
argument to automatically display the plot before saving it. We want a flag
that toggles on and off -- consult the
argparse
docs to see how to set theaction
.
Advanced:
- If you set a version for your package, add a
--version
argument with thestore_true
action. If this is set, print the version andreturn
Command line arguments are really great for quickly trying out options, but it can be tricky to record what options were used for a particular run. Instead, it's often much better to use input files. These can be kept alongside the corresponding outputs, and shared with others to enable them to reproduce and build on top of your work.
There are many different file formats that you might consider when choosing one for your input files. Some that you might consider:
- JSON: very popular, implementations in most programming languages, but lacks comments and can be difficult for humans to write
- YAML: also very popular with many implementations, but has several gotchas and is notoriously complex
- INI: simple and flexible, with a loose syntax, with many different dialects or flavours
- TOML: essentially INI with a formal syntax
We've briefly encountered TOML with our pyproject.toml
, and from
Python 3.11 it's in the standard library.
For earlier versions of Python, we can use the tomli
library. This is very fast, although the downside is that it can't
write files. For this exercise, that won't matter.
Using tomli
is pretty simple: tomli.load()
works with file handles
(as returned from open
-- but note we need to open in "rb"
mode)
and tomli.loads()
works with strings directly. Both of them return a
Python dict. The simplest way to read a file is like this:
with open(filename, "rb") as f:
data = tomli.load(f)
- Add
tomli
to your project dependencies, and thenpip install
your project to automatically download it- Don't forget to use
--editable
or-e
while developing!
- Don't forget to use
- Add a new argument
--filename
to yourparser
. You'll want to passdefault=None
toadd_argument
so that it has some default value. Theargparse
docs have some more information on this argument. - After parsing the command line arguments, if a filename has been
passed, use
tomli.load
to read the input file - Extract the Miller parameters from the data, use them instead of the other command line arguments
Here's an example input file that your program should be able to handle:
# An example input file
[miller]
A = 2.2 # Aspect ratio
kappa = 3.0
delta = 0.7
R0 = 3.2
Bonus:
- Try to avoid calling
flux_surface
in two different places. Code is often easier to maintain if you don't repeat yourself. - Can you allow command line arguments to override values set in the input file?