Separating the UI from the functionality
A programming principle called model-view-controller (MVC) is recommended when designing code. Under MVC, programs are separated into one of three components, the model (library) which provides the core functionality, a viewer which provides a visualisation of the results, and a controller which links the user to the model. In our test code the model would be the two functions generate_positions
and write_file
, whilst the controller would be the command line interface. [In this example we could consider the file output to be the viewer.]
At the end of cycle1 we had a single file, sim.py
, which contained both the model and the controller. Our first task will be to separate this into two parts. The first part will be a python module which provides the functionality, and the second will be a script which receives user input and calls the library functions.
Creating a python module
Python modules, like the numpy
module that we have already used, can be easily created by obeying a simple directory/file structure. If we want to create a module called skysim
then all we need to do is create a directory with the same name, and add an empty file called __init__.py
. Let’s do that now:
$ mkdir skysim
$ touch skysim/__init__.py
To access the module we simply use import skysim
.
$ python
>>> import skysim
>>> dir(skysim)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']
>>>
We can see that the skysim
module already has some attributes defined, and they all start with a double underscore ( __
or ‘dunder’ ). The __file__
attribute is a string containing the full path to the file __init__.py
. The __name__
attribute will contain the string skysim
because this is the name of the module. If we had renamed the module on import (using import skysim as other
) then the __name__
attribute would still be the same. Feel free to explore the other attributes.
In order to add some functions or attributes to our module we can simply add our sim.py
file to the skysim
directory. If we do this and then restart our python interpreter we can import all the functions/modules/variables provided by sim.py
by doing from skysim import sim
. For example:
$ python
>>> from skysim import sim
>>> dir(sim)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'argparse', 'generate_positions', 'math', 'np', 'write_file']
>>>
Above we can see the two functions that we defined, as well as the modules that we imported (argparse
, math
, and np
). We now have a library that we can import. When we import a module, all the code within that file is executed. In the case of our sim.py
file this means that we import some other modules and then define some functions.
If a python file is run via python myfile.py
then the __name__
attribute will be set to the string __main__
. However if a python file is imported via import myfile
or import myfile as mf
, then the __name__
attribute is set to be the filename (without the .py
extension). Therefore when we import sim.py
as part of our module the CLI that we created with argparse does not get executed because we hid it within an if __name__ == "__main__"
clause.
Creating a stand alone script
To create a script that calls this library we create a new file called sim_catalog
. We then move all the content of the if __name__ == "__main__"
clause out of sim.py
and into our new file. Finally, we the import the functions that we need from our newly created module.
Our new script looks like this:
#! /usr/bin/env python
import argparse
from skysim.sim import generate_positions, write_file
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()
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)
Note that we have a shebang line (#!) to indicate that we want to use the python interpreter. This means that we can make the file executable and then execute it like any other program without the user having to explicitly type python
. It is not shown here, but I have made the file executable so that I can just type ./sim_catalog
to run the above code.
We have now separated our interface (sim_catalog
) from the model (skysim.sim
). Currently the user will not notice any difference because the functionality hasn’t changed. However, we are now able to import the model into other python scripts. Our code is becoming easier to re-use by ourselves (and other developers).
Updating our test script
Finally, we just need to update our test script so that it will use the new sim_catalog
script to do the testing.