Solution Development in Python, Part 2

Continuing on from part 1, we now have a nice little package that we wrote. Let’s refine this package to be a little more in line with Python practices, add some tests (well, a test), and provide some console execution. You’re probably not going to just have one file in your solution and even if you did, that file would not be __init__.py, in terms of where you store all your logic. That was enough to get us started but we can do better than that. Let’s add a file called saying.py. This file should go in the inner proverb directory. Take the logic that was in __init__.py and move it to this new file, so it looks like this: This leaves the init file temporarily empty. Very temporary as it turns out since you can now add this to your __init__.py file: Here I’m just making the saying() function available on the module.

(Re)Install It

If you wanted to install your package again to see if it works, you can try this:
pip install .
This is the same thing we did last time. However, you are likely to encounter a message saying something like: “Requirement already satisfied (use –upgrade to upgrade).” What this is saying is that some package called “proverb” at version “0.1” (assuming you used the same version I did) is already in place. It’s basically telling you what to do:
pip install . --upgrade
You could also, of course, change the version and, in fact, that is what you are likely to do when you make changes like we did. Logically, we haven’t changed how our package works so it would run the same way as before:
>>> import proverb
>>> print(proverb.saying())

Add a Dependency

Let’s add another package that our package will depend on. If you’re writing a test solution, you will likely be relying on other packages. So let’s see how this is done. The common contrivance in these kind of simple tutorials is to use a formatting language. So let’s use Markdown. Specifically, our package will depend on the markdown package. We have to add this dependency into our setup.py file. Note that if you were writing a test solution for automating browsers, this is how you would include the selenium package. Now let’s change our saying.py file to import this package and then use it. Change the code in that file as such: Notice that the return statement now calls markdown(). Further, some lines of the saying have markdown characters. You obviously won’t see this formatting if running from certain command lines but you will see the tags that were generated from the markdown. The goal here, however, was just to show you how to incorporate another package into your own. This notion of dependencies does bring up something you’ll hear about and see quite often regarding Python packages: a requirements.txt file.

setup.py or requirements.txt?

There can be confusion regarding these two files. What’s important to know is that you’ll have what are called “abstract dependencies” in your setup.py file and you’ll have “concrete dependencies” in your requirements.txt file. The idea is that a dependency is “abstract” when it’s stated only as a name, possibly with an optional version specifier attached. A dependency is “concrete” when it also specifies where the dependency should be gathered from or what specific version should be used. The main thing to really understand is that a pip install command, which someone will likely use to install your package, does not look at the requirements.txt file by default; instead it only looks at an “install_requires” section in setup.py. However, where you will see requirements.txt files become handy is if someone is using your package in a virtual environment or if they want to contribute to developing your application. Some people like to have both files, however, and I personally think this makes sense. The nice thing is that you can maintain your dependencies in requirements.txt and then have setup.py use that file. You can also just have pip use the file, like this:
pip install -r requirements.txt
But, again, why? Why have two files? The core problem is that pip has no dependency resolver. So pip, at the time I write this, uses the first specification (and thus version) that it finds for a project package. Requirements files are used to force pip to properly resolve dependencies. I won’t go into that too much here. For now, just be aware of this.

Command Line Scripts

If you provide test solutions, you might provide a script that allows someone to use your solution as a test runner. This means your users need a way to run that script from the command line. There is one approach where you create a bin directory, put a Python script file in there, and then use a “scripts” keyword in your setup.py file. I’m not going to do that. Instead I’m going to take another approach, which involves using what’s called an entry point. In this case, the entry point is referred to as “console_scripts”. This allows Python functions to be directly registered as command-line accessible tools. Note that it’s a Python function that is registered as a command line statement to be executed. If you instead wanted, say, a bash shell script, then you would have to go the “scripts” route with a “bin” directory. In your inner proverb directory, create a file called command_line.py and put the following in it: You can test the “script” by running it directly:
>>> import proverb.command_line
>>> proverb.command_line.main()
Now the main() function can then be registered in setup.py. Make the following change to that file: Once the package has been installed with this modification in place, you will now have a command line executable available to you called “proverb-saying” which you can run directly at the command line. In Windows, this will be an executable called “proverb-saying.exe”. Do note this does require that the location where your Python scripts are stored is part of the path. This largely depends on how you installed Python and what particular Python distribution you installed.

Testing

I most definitely am not going to go into every nuance of unit testing your solutions with Python; at least not in this series. But I do want to include at least one test just to show you how to do it. These tests should generally be placed in a submodule of your project. The reason for this is that this approach means your tests can be imported, but they won’t pollute the global namespace. So here’s an example of where you can create a tests directory:
proverb
    tests
        __init__.py
    proverb
	  __init__.py
             command_line.py
             saying.py
setup.py
Notice you have an __init__.py to mark the directory as a module. In this case, that file can be empty. Now create a file called test_saying.py and put the following in it: The Python testing ecosystem is large so I’m not going to cover much about it here. What I will say is that I’ll use Nose for the test runner. Let’s install it:
pip install nose
Now you can run it from the directory of your project:
nosetests
You should see that the single test passes. That’s great but let’s make sure we can run our tests via our setup.py as well as make Nose a dependency. Modify setup.py as follows: Then, to run tests, you can do the following:
python setup.py test
And that should cover us for now. In this post, we’ve updated our project to use a dependency, to provide a command-line script, and to incorporate some tests. This is important stuff when you are creating a test solution in Python. So here I’ve shown you the simplest way to get started with these and (hopefully) gain some confidence. In the next, and last, part of this series I’ll cover packaging up your solution and deploying it. Join me for the finale in part 3.
Share

This article was written by Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.