The Code Itself¶
In this section you will:
Put some scientific code in your new Python package.
Update your package’s list of dependencies in
requirements.txt
.Write a test and run the test suite.
Use a “linter” and style-checker.
Commit your changes to git and sync your changes with GitHub.
A simple function with inline documentation¶
Let’s write a simple function that encodes Snell’s Law and include it in our Python package.
Look again at the directory structure.
example/
├── .flake8
├── .gitattributes
├── .gitignore
├── .travis.yml
├── AUTHORS.rst
├── CONTRIBUTING.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs
│ ├── Makefile
│ ├── build
│ ├── make.bat
│ └── source
│ ├── _static
│ │ └── .placeholder
│ ├── _templates
│ ├── conf.py
│ ├── index.rst
│ ├── installation.rst
│ ├── release-history.rst
│ └── usage.rst
├── example
│ ├── __init__.py
│ ├── _version.py
│ └── tests
│ └── test_examples.py
├── requirements-dev.txt
├── requirements.txt
├── setup.cfg
├── setup.py
└── versioneer.py
Our scientific code should go in the example/
subdirectory, next to
__init__.py
. Let’s make a new file in that directory named
refraction.py
, meaning our new layout will be:
├── example
│ ├── __init__.py
│ ├── _version.py
│ ├── refraction.py
│ └── tests
│ └── test_examples.py
This is our new file. You may follow along exactly or, instead, make a file with a different name and your own scientific function.
# example/refraction.py
import numpy as np
def snell(theta_inc, n1, n2):
"""
Compute the refraction angle using Snell's Law.
See https://en.wikipedia.org/wiki/Snell%27s_law
Parameters
----------
theta_inc : float
Incident angle in radians.
n1, n2 : float
The refractive index of medium of origin and destination medium.
Returns
-------
theta : float
refraction angle
Examples
--------
A ray enters an air--water boundary at pi/4 radians (45 degrees).
Compute exit angle.
>>> snell(np.pi/4, 1.00, 1.33)
0.5605584137424605
"""
return np.arcsin(n1 / n2 * np.sin(theta_inc))
Notice that this example includes inline documentation — a “docstring”. This is extremely useful for collaborators, and the most common collaborator is Future You!
Further, by following the numpydoc standard, we will be able to automatically generate nice-looking HTML documentation later. Notable features:
There is a succinct, one-line summary of the function’s purpose. It must one line.
(Optional) There is an paragraph elaborating on that summary.
There is a section listing input parameters, with the structure
parameter_name : parameter_type optional description
Note that space before the
:
. That is part of the standard.Similar parameters may be combined into one entry for brevity’s sake, as we have done for
n1, n2
here.There is a section describing what the function returns.
(Optional) There is a section of one or more examples.
We will revisit docstrings in the section on Writing Documentation.
Update Requirements¶
Notice that our package has a third-party dependency, numpy. We should
update our package’s requirements.txt
.
# requirements.txt
# List required packages in this file, one per line.
numpy
Our cookiecutter configured setup.py
to read this file. It will ensure that
numpy is installed when our package is installed.
We can test it by reinstalling the package.
python3 -m pip install -e .
Try it¶
Try importing and using the function.
>>> from example.refraction import snell
>>> import numpy as np
>>> snell(np.pi/4, 1.00, 1.33)
1.2239576240104186
The docstring can be viewed with help()
.
>>> help(snell)
Or, as a shortcut, use ?
in IPython/Jupyter.
In [1]: snell?
Run the Tests¶
You should add a test right away while the details are still fresh in mind. Writing tests encourages you to write modular, reusable code, which is easier to test.
The cookiecutter template included an example test suite with one test:
# example/tests/test_examples.py
def test_one_plus_one_is_two():
assert 1 + 1 == 2
Before writing our own test, let’s practice running that test to check that everything is working.
Important
We assume you have installed the “development requirements,” as covered in Getting Started. If you are not sure whether you have, there is no harm in running this a second time:
python3 -m pip install --upgrade -r requirements-dev.txt
python3 -m pytest
This walks through all the directories and files in our package that start with
the word ‘test’ and collects all the functions whose name also starts with
test
. Currently, there is just one, test_one_plus_one_is_two
.
pytest
runs that function. If no exceptions are raised, the test passes.
The output should look something like this:
======================================== test session starts ========================================
platform darwin -- Python 3.6.4, pytest-3.6.2, py-1.5.4, pluggy-0.6.0
benchmark: 3.1.1 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /private/tmp/test11/example, inifile:
plugins: xdist-1.22.2, timeout-1.2.1, rerunfailures-4.0, pep8-1.0.6, lazy-fixture-0.3.0, forked-0.2, benchmark-3.1.1
collected 1 item
example/tests/test_examples.py . [100%]
===================================== 1 passed in 0.02 seconds ======================================
Note
The output of pytest
is customizable. Commonly useful command-line
arguments include:
-v
verbose-s
Do not capture stdout/err per test.-k EXPRESSION
Filter tests by pattern-matching test name.
Consult the pytest documentation for more.
Write a Test¶
Let’s add a test to test_examples.py
that exercises our snell
function.
We can delete test_one_plus_one_is_two
now.
# example/tests/test_examples.py
import numpy as np
from ..refraction import snell
# (The above is equivalent to `from example.refraction import snell`.
# Read on for why.)
def test_perpendicular():
# For any indexes, a ray normal to the surface should not bend.
# We'll try a couple different combinations of indexes....
actual = snell(0, 2.00, 3.00)
expected = 0
assert actual == expected
actual = snell(0, 3.00, 2.00)
expected = 0
assert actual == expected
def test_air_water():
n_air, n_water = 1.00, 1.33
actual = snell(np.pi/4, n_air, n_water)
expected = 0.5605584137424605
assert np.allclose(actual, expected)
Things to notice:
It is sometime useful to put multiple
assert
statements in one test. You should make a separate test for each behavior that you are checking. When a monolithic, multi-step tests fails, it’s difficult to figure out why.When comparing floating-point numbers (as opposed to integers) you should not test for exact equality. Use
numpy.allclose()
, which checks for equality within a (configurable) tolerance. Numpy provides several testing utilities, which should always be used when testing numpy arrays.Remember that the names of all test modules and functions must begin with
test
or they will not be picked up by pytest!
See Common Patterns for Tests for more.
“Lint”: Check for suspicious-looking code¶
A linter is a tool that analyzes code to flag potential errors. For example, it can catch variables you defined by never used, which is likely a spelling error.
The cookiecutter configured flake8
for this purpose. Flake8 checks for
“lint” and also enforces the standard Python coding style,
PEP8. Enforcing
consistent style helps projects stay easy to read and maintain as they grow.
While not all projects strictly enfore PEP8, we generally recommend it.
Important
We assume you have installed the “development requirements,” as covered in Getting Started. If you are not sure whether you have, there is no harm in running this a second time:
python3 -m pip install --upgrade -r requirements-dev.txt
python3 -m flake8
This will list linting or stylistic errors. If there is no output, all is well. See the flake8 documentation for more.
Commit and Push Changes¶
Remember to commit your changes to version control and push them up to GitHub.
Important
The following is a quick reference that makes some assumptions about your local configuration and workflow.
This usage is part of a workflow named GitHub flow. See this guide for more.
Remember that at any time you may use git status
to check which branch
you are currently on and which files have uncommitted changes. Use git diff
to review the content of those changes.
If you have not already done so, create a new “feature branch” for this work with some descriptive name.
git checkout master # Starting from the master branch... git checkout -b add-snell-function # ...make a new branch.
Stage changes to be committed. In our example, we have created one new file and changed an existing one. We
git add
both.git add example/refraction.py git add example/tests/test_examples.py
Commit changes.
git commit -m "Add snell function and tests."
Push changes to remote repository on GitHub.
git push origin add-snell-function
Repeat steps 2-4 until you are happy with this feature.
Create a Pull Request — or merge to master.
When you are ready for collaborators to review your work and consider merging the
add-snell-function
branch into themaster
branch, create a pull request. Even if you presently have no collaborators, going through this process is a useful way to document the history of changes to the project for any future collaborators (and Future You).However, if you are in the early stages of just getting a project up and you are the only developer, you might skip the pull request step and merge the changes yourself.
git checkout master # Ensure local master branch is up to date with remote master branch. git pull --ff-only origin master # Merge the commits from add-snell-function into master. git merge add-snell-function # Update the remote master branch. git push origin master
Multiple modules¶
We created just one module, example.refraction
. We might eventually grow a
second module — say, example.utils
. Some brief advice:
When in doubt, resist the temptation to grow deep taxonomies of modules and sub-packages, lest it become difficult for users and collaborators to remember where everything is. The Python built-in libraries are generally flat.
When making intra-package imports, we recommend relative imports.
This works:
# example/refraction.py from example import utils from example.utils import some_function
but this is equivalent, and preferred:
# example/refraction.py from . import utils from .utils import some_function
For one thing, if you change the name of the package in the future, you won’t need to update this file.
Take care to avoid circular imports, wherein two modules each import the other.