Installable Single Package
00:00 In the previous lesson, I talked about a simple package revolving around a single-file program. In this lesson, I’m going to make it more complicated and add more code and see how that changes the approach. First off, if you’ve got more than one coding file, you’re probably going to want to group it together into a module.
00:18 I’m going to take the example that I had before with a single file that had a Hello World in it and add a utilities file to go with it. When you have multiple files, typically that means you also want multiple test files.
00:30 You’ll also want to start doing things like checking your test coverage and running a linter to help you catch problematic code. The package itself doesn’t change by all that much. Here’s an example.
00:41
helloworld.py
is now inside of a directory called helloworld
—that’s the module. Because it’s a module, it has to have a __init__
file and the promised utils.py
file that I talked about before, splitting up helloworld
into multiple units.
00:58
Likewise, the tests
now also becomes a directory. It’s a module as well so that it can be run. It also contains a __init__
. There’s a specific file for testing the helloworld
file, and a specific file for testing utils
. runtests
isn’t necessary, but it’s something I tend to do because I get tired of typing out the long python -m unittest
command, and particularly if you start doing things like running coverage tests—which I’ll show you how to do—there’s a little more logic to it, so I create a little Bash file to help with that. The rest of the files here that I’ve grayed out are the same files that were there in the previous package.
01:36
This is my new version of Hello World. It’s very, very similar to the previous one. All that’s changed is I’m now importing code from the utils
library inside of helloworld
. So, I’m now using a module helloworld
with a utils
file inside of it and grabbing the function show_message()
.
01:52
Then, I’m replacing what was a print()
function on line 14 with the show_message()
function. show_message()
is just a wrapper for print()
—utils
is a really simple file—but you can see how the two files interact inside of the module together.
02:06
What was formerly tests.py
has been moved inside of a tests
module and it has been renamed to TestHelloWorld
to match with the helloworld.py
function.
02:16
This changes how things are imported, so line 8 has changed: importing the do_hello
and URL
from the file that’s been moved. The other thing that has changed in this file is there’s some stuff missing.
02:29
Because I’m going to be using the runtests
and coverage
—which I’ll show you in a minute—I no longer need the '__main__'
section at the bottom of the file for execution. The last thing I changed was—you may recall—helloworld
used to have a __version__
variable inside of it.
02:44
I’ve moved this into the __init__
file—this makes it easier to grab and will simplify some of the code inside of setup.py
, which I’ll show you in a few minutes.
02:56 This entire lesson is on writing more code with a larger module, which means you really should be writing more tests to go with it. Once you’re writing a lot of tests, it’s a good idea to figure out whether or not your tests are actually doing their job.
03:08
One measure of this is the coverage. Coverage tells you what percentage of your code has actually been executed by the tests. There’s some great libraries out there to help you with this; one of the most popular ones is simply enough called coverage
, and it can be found by installing pip install coverage
.
03:29 Here’s some sample output from the coverage report. This particular example is where I’ve commented out some of the test code so that I don’t get 100% coverage.
03:37 The first line here is the output from the program. I’ve actually commented out the line that does something, which is why this is empty. The second section is Python telling me that the tests have run.
03:48
One of my tests is empty, but of course, it passes when it’s empty. And the final section here is the simple report from coverage
. You can see helloworld.py
was executed, two of the lines inside of it were missed and not run, and it only has 75% coverage.
04:04
You can get more information by running the coverage
command with the html
argument to get a full report inside of your browser.
04:12 My little script here actually reminds me that this is what the command is for that. I’ll show you an example.
04:19
And here it is—a code-highlighted version of the helloworld.py
. It highlights what was run, what’s missing, and anything that’s been excluded.
04:28
It’s very hard to test the __main__
execution of a script inside of your tests files, so instead of measuring it, I test the do_hello()
function specifically and then do a # pragma: no cover
so that coverage
doesn’t actually examine this line.
04:43
This is the Bash script that I was talking about earlier that actually runs the coverage command. First off, I do a quick find
and remove any .pyc
files.
04:52
This stops anything from being cached and causing strange behaviors. This is overkill, but it’s saved my bacon on a couple occasions. The actual coverage
command is very similar to doing the -m unittest
with Python directly, except that you also pass in information on what modules to look for source code in. This line itself is sufficient to get the coverage information, but then as you’ve seen inside of the results, I create the simple coverage report and then echo
some of this information out in an easier-to-read fashion.
05:24
If you’re using a linter like pyflakes
or pep8
, this is a good place to put it so that it runs after you’ve run your tests every single time. The last thing that I want to show you in this lesson is just some simplification inside of setup.py
. In the previous version, there were four or five lines that read in the helloworld.py
file and grepped out the __version__
value.
05:46
Now that __version__
is put inside of __init__
, it can very easily be imported directly, and this removes four or five lines from the setup.py
.
05:55 So, this is just a little bit easier.
05:58 This lesson talked about the file structure changes in going from a single-file program to a multi-file module. In the next lesson, I’m going to make things even more complicated and add multiple modules.
Dan Bader RP Team on May 13, 2020
@Peter: That is not true, please see this section in the official Python 3.8 docs for example:
The
__init__.py
files are required to make Python treat directories containing the file as packages.
Become a Member to join the conversation.
Peter on May 7, 2020
Outdated information: Python modules don’t have to have init.py files since Python 3.3, released in 2012.