Test every unicode method on every object for every model
Tuesday, February 23rd, 2010
Tags: django, testing.
I'm working on a relatively new Django-based website now as a personal project. It'll be an open source project that I hope to interest others in, so I'm keen to get certain things right. One of my goals is 100% test coverage for the project. When I first started programming, I couldn't see the point of this. Suffice it to say, I've since learned my lesson.
Take the following extremely simple model:
class Post(models.Model):
title = models.CharField(max_length=100)
subtitle = models.CharField(max_length=100, null=True, blank=True)
body = models.TextField()
def __unicode__(self):
return self.title
It wasn't immediately clear when I started, for example, why I would bother to test the __unicode__ method on a model. But then, as if to mock my hubris, the programming gods tricked me several thousand times into changing the name of the field referenced in a __unicode__ method without remembering to change it in the __unicode__ method itself. And this wouldn't show up until the admin would crash or something equally irritating. Or I would decide to make the __unicode__ method slightly more complicated:
def __unicode__(self):
if self.subtitle:
return "%s: %s" % (self.title, self.subtitle)
return self.title
thereby multiplying the possibilities for stupid, annoying little errors that would sometimes escape my notice until later on. Of course this happens most in the early stages of a project (when you're least likely to have tests), but errors like this can creep in later on too.
So, it's a nice idea to make sure that you test every __unicode__ method---even the simplest kind---by at least calling it for every model on a number of objects in your database. Luckily, this doesn't need to be done manually for every model. Django's super-awesome powers of introspection allow us to easily iterate over every object for every model in our entire project. Here's the test method I use to do that.
from django.db.models.loading import cache from django.test import TestCase no_unicode_method = ['Session', 'LogEntry']class TestUnicodeMethods(TestCase): """Sanity check on unicode methods. You catch a surprising number of errors (especially in the early stages of development) just by calling the unicode method on every object for every model.""" for model in cache.get_models(): meta = model._meta if meta.object_name not in no_unicode_method: objs = model.objects.all() for obj in objs:
obj.__unicode__()
Obviously, if you have some fancy-schmancy __unicode__ method, you'll want to give it a proper test of its own. But this single test can be helpful, especially when you're getting your project off the ground.
UPDATE: Now that I think about it, I hope I haven't given the impression that this is a substitute for a proper test, which would actually compare the result of calling the method with an expected result. All this does is test that you can call it. So perhaps it makes sense to say that this exercises your code a bit. I'm thinking about writing a few more general tests that exercise your code in this way (say, by using the test client to visit every page on your admin, as a way of making sure that the fields in your models.py and admin.py files don't conflict) and bundling it up together in an app. Hmmm, django-calisthenics, perhaps?
Remove .pyc files as a step in the test process
Wednesday, December 9th, 2009
Tags: python, testing.
I got burned the other day at work by a dangling .pyc file. Although I had deleted the corresponding .py file, the tests failed to register that I was still importing it elsewhere because they were finding the .pyc file and running on their merry way. We only found this because my boss and I were running the tests at the same time on different computers while chatting on IM, and he had properly deleted the .pyc file too.
Anyway, to prevent this from happening again I inserted this at the beginning of our test script:
#Zap .pyc files so our tests aren't fooled if we delete a .py file
#and forget to delete the corresponding .pyc file.
for path, dirs, files in os.walk(os.path.dirname(__file__)):
for f in os.listdir(path):
if re.search('\.pyc$', f):
fullpath = os.path.join(path, f)
print "removing %s" % fullpath
os.remove(fullpath)
(The test setup script in this case is in the top level directory. Obviously you'll want to adapt the directory that os.walk works on to your own circumstances.)
This seems a sensible thing to do---so sensible, in fact, that I'm wondering why it's not a standard practice to zap .pyc files before running your tests. (It only adds a tenth of a second at most to the process.) If I'm missing something, and especially if there's some good reason not to do this, please let me know in the comments, and I'll update (or, in the case of maximum embarrassment, delete) the post.
UPDATE: Oh, I just noticed the django-command-extensions has a clean_pyc command. The code is nicer than what I have above too. Here's the relevant bit:
def handle_noargs(self, **options):
project_root = options.get("path", None)
if not project_root:
project_root = get_project_root()
exts = options.get("optimize", False) and [".pyc", ".pyo"] or [".pyc"]
verbose = int(options.get("verbosity", 1))>1
for root, dirs, files in os.walk(project_root):
for file in files:
ext = os.path.splitext(file)[1]
if ext in exts:
full_path = _j(root, file)
if verbose:
print full_path
os.remove(full_path)
(The _j is just os.path.join.)
3 comments on this post so far. Leave a comment
Dynamically Constructing "Or" Queries with Django's ORM
Sunday, December 6th, 2009
A quick point for people who are already familiar with Django's ORM.
Suppose I have a model:
class Entry(models.Model):
title = models.CharField(max_length=50)
text = models.TextField()
tag = models.ManyToManyField(Tag)
and I want to query all the entries that have a particular tag. This is easy:
>>> entries = Entry.objects.filter(tag='good_times')
I can also encapsulate that filter using a Q object:
>>> from django.db.models import Q >>> q = Q(tag='good_times') >>> good_times_entries = Entry.objects.filter(q)
(The documention on Q objects is here. You can import Q directly from django.db.models (it is imported into that module's init.py), but the code for it, if case you want to take a look, actually lives in django.db.models.query_utils.)
Now suppose I want to construct a query using tags as filters, for some number of tags that won't be determined until runtime.
If I want to filter every entry that has the tag 'good_times' AND the tag 'bad_times' AND 'you_know_Ive_had_my_share', I don't even need a Q object (though I could use one). I can use the "in" field lookup:
>>> taglist = ['good_times', 'bad_times', 'you_know_Ive_had_my_share'] >>> my_entries = Entry.objects.filter(tag__in=taglist)
This obviously doesn't require me to know in advance how many tags I'll be using. So I can construct my list however I like (using user input, for example), and then use it in my query.
Recently I had the problem of needing to dynamically construct an "or" query. I'm not sure if the following solution is the best one. If there's something simpler, please let me know in the comments. (The basic idea is due to some clever person on Stack Overflow, but I had trouble finding the answer in the first place, and now I can't find it again to give proper credit.)
In any case, this works:
>>> taglist = ['good_times', 'bad_times', 'you_know_Ive_had_my_share']
>>> from django.db.models import Q
>>> q = Q()
>>> for tag_in_list in taglist:
q |= Q(tag=tag_in_list)
>>> or_entry_query = Entry.objects.filter(q)
Voila! This is the sort of thing that looks obvious to me once I see it. The documentation on the Q object says "Q objects can be combined using the & and | operators. When an operator is used on two Q objects, it yields a new Q object." It took a while for me to clue in to the fact that it would permit this too.
No comments on this post so far. Leave a comment
A very, very gentle introduction to the Linux Command Line
Saturday, November 28th, 2009
Tags: command_line, linux, ubuntu.
I recently set a friend of mine up with a fresh install of Ubuntu on her laptop. Ubuntu is such a user friendly version of Linux that you can go a long way without having to touch the command line. Still, I thought it would be good to give her a few tips about using the command line, since sooner or later she might end up needing it (say, at a web page that only tells her how to install software from the command line). Anyway, it's fun and liberating to learn something new. And then I thought I might as well post a tutorial here in case it helps someone else (even though there are already about a hundred thousand of them out there already).
Just a warning, I'm fairly new to all this stuff myself, so if you have corrections or suggestions, please feel free to add them in the comments.
Getting Around
OK, if you're using Ubuntu, open up the Applications menu, click "Accessories" and then select "Terminal." (If you want handy access to the terminal in the future, you can right click on it in the menu and add it to the launcher panel. A little icon will appear up where the Firefox icon is.)
You should follow along with the tutorial.
Here's what my prompt looks like:
chris@bobo:~$
Yours will no doubt look different. My user name is "chris" and my laptop's name is "bobo," which was also a nickname for he of beloved memory Different terminals can be customized in all sorts of ways, so yours might have the date, or display exactly where you are in the file system at all times, etc.
The first question when we get to some new place is "Where am I?" We can learn the answer to this at the command line by asking the computer to print our working directory:
chris@bobo:~$ pwd /home/chris
Each user gets his or her own home directory. It's where we keep our files and it's where we start when we open up a new terminal.
What's in here? Well, let's list the contents of the directory:
chris@bobo:~$ ls code Desktop Documents Dropbox Music Pictures workspace ...
Lots of stuff.
Now, often when you read instructions about how to do something or other, you'll be asked to navigate to a directory. Chances are that it won't be your home directory. To do this, we'll want to change our directory. Let's try changing to my desktop:
chris@bobo:~$ cd Desktop/ chris@bobo:~/Desktop$
It worked! Notice that my prompt changed to indicate where I am in my file system. The tilde symbol is a convenient way of writing my home directory (/home/chris). So "~/Desktop" actually means "/home/chris/Desktop"
At this point you might be thinking that people who use Linux are idiots, since this is a lot of typing, when a few clicks of your mouse would have got you there much faster. In fact, people who use Linux tend to hate typing, and if you find yourself doing much typing you should ask yourself if you're missing a convenient shortcut. Let's back up and try again:
chris@bobo:~/Desktop$ cd ..
The two dots tell us to move up a level in our directory structure. Remember this: It's useful. (Remember also that you can always pwd to check where you are, if your prompt doesn't already tell you.)
Now, let's try navigating into the desktop again, but this time with less typing. So type this and then the tab key:
chris@bobo:~$ cd D
(It's case sensitive.) If "Desktop" is the only folder in your home directory that starts with "D," then it should expand to the whole word when you hit the tab key. Notice above that I actually have three directories that start with "D," so nothing happens. However, if I hit it again, I'll see this:
Desktop/ Documents/ Dropbox/
Now I can type an additional character, "e," and then tab, and the letters should expand to "Desktop." Then I can hit enter and be on my merry way.
Using tab completion, you can race through your file system. Because if there's another folder within Desktop, you can either enter the first letter or two and then tab, or just hit tab twice to see the options and then type and tab.
Two other useful tips for navigation: First, typing:
chris@bobo:~$ cd -
Will take you to the previous directory you were in. And second, typing:
chris@bobo:~$ cd ~
will always take you back to your home directory.
Let's create a folder (make a directory) on your desktop and then navigate to it:
chris@bobo:~$ mkdir Desktop/my_awesome_folder chris@bobo:~$ cd Desktop/my_awesome_folder
(If you want spaces in the folder name, use quotation marks around the entire path.)
Now let's go home again:
chris@bobo:~$ cd ~
Leave your terminal now and check to make sure that "my_awesome_folder" exists on your Ubuntu desktop.
Clutter is annoying. Let's get rid of it. Hey! Hands away from the mouse. We'll remove it from the command line:
chris@bobo:~$ rmdir Desktop/my_awesome_folder
Try monkeying around with all this for a while until you feel comfortable. Notice that you can go above your home directory in your computer. Feel free to explore and then come back to your home directory.
OK, now what did you do? Well, don't rely on your memory---just check your history:
chris@bobo:~$ history ... 8538 cd Desktop/ 8539 cd ~ 8540 history
This will give you a numbered list of the commands you've used. This is very handy if you've done something clever or useful or dangerously stupid and then forgotten what it was, exactly. You can always execute a command from the list by typing an exclamation mark plus the number:
chris@bobo:~$ !8539 cd ~
Here I just told the computer to change to my home directory, which was pretty useless since I was already there.
You can also scroll through previous commands by using the up and down arrows. And two explanation marks just repeats the last command.
Now let's read a text file. (Notice that you won't be able to read some types of files, e.g., MS Word documents, using this technique.) First we need to create something so that you can read it.
chris@bobo:~$ ls > myls.txt
(What that did was run the "ls" command and then write the output to a file called "myls.txt.")
To read this file at the command line, we use this command:
chris@bobo:~$ cat myls.txt
(Did you just type out all of "myls.txt"? Shame on you! Remember tab completion!)
This command is called "cat," I believe, because it concatenates multiple files and prints them to the standard output. In this case there's only one file, so it just prints it to the standard output.
We don't want that file cluttering up our file system, so let's remove it:
chris@bobo:~$ rm myls.txt
(Did you just type out all of "myls.txt"? Double shame on you! I just told you to remember tab completion!)
Installing Stuff
OK, now let's install something! Ubuntu maintains an extremely helpful repository of software packages and makes it very easy to install them. Here's a package that is essential to working with the Python programming language, the subject of a future post (don't worry if you're not interested, since it's small and installing it is harmless). Here's how we install it:
chris@bobo:~$ sudo apt-get install python-setuptools
(We don't have to be in our home directory. We can use apt-get from anywhere in our file system.)
What's going on here? Well, the first bit, "sudo" is telling the computer that we know we're trying to do something that requires enhanced permissions. The second bit, "apt-get" is the command for Ubuntu's special package repository. What are we going to do here? Install, of course, which is the third part of our command. What follows are all the different packages we want to install, separated by a space. In this case it's just one.
Try it, and then enter your password when prompted. (You may also be asked whether you want to use up the space.)
OK, now do it again.
(I hope you didn't type out the command again, since that would have been exhausting. You just hit the up arrow and then enter, right?)
Notice that this time your system simply tells you that it's already installed. This is handy, since sometimes someone will give you some incredibly long list of things to install. You don't need to worry whether you already have them, since the computer will simply skip the ones you already have.
Sometimes you have a rough idea of what the package you want to install is called, but you're not quite sure. Suppose you're curious about what the resources are in the repository for, say, greek:
chris@bobo:~$ sudo aptitude search greek
Piping
Now you know how to navigate around your file system, install software, and then get the history of what you've done. There's one last idea I want to introduce: piping.
The idea in piping is that we can take the output of one program and pipe it into another. And, if we want, we can take the output of that second program and pipe it into a third, and so on all the live-long day. This may sound very modest, but arguably the power and the beauty of the Linux operating system lies in the possibilities that this creates.
A few very basic examples will give a hint of what I mean. First, notice that my history above had 8539 entries. That's a lot of entries! What if I want to know what I've installed using apt-get, and I don't want to see any other entries? Well, why not pipe the output of history into a program that searches that history for the string of characters "apt-get"?
chris@bobo:~$ history | grep apt-get
Remember that we produce the output of the history program simply by typing "history." The vertical bar is the pipe. So each line produced by history goes into our search program, called "grep," which searches for the string "apt-get." And so you'll see something like:
... 8528 sudo apt-get install python-setuptools 8552 history | grep apt-get
The line you just typed even includes itself, since it too includes the search string "apt-get" and is in the history as soon as you hit enter.
Suppose that the output of the above command is still too long. Well, let's just pipe our output into a program that feeds us the output of any program in manageable chunks!
chris@bobo:~$ history | grep apt-get | less
The program is called "less." Once we type this, we briefly enter less to read our text. We can page forward by hitting the space bar and back by hitting the "b" character. When we're done reading, we just hit the "q" character.
less is a very handy program, and you'll find yourself using it a lot. Recall above that you used cat to read the file. Since your home directory is probably pretty skimpy, chances are the entire file fit on a single screen. But if it had been long, it would have flown by too fast to read. In that case, you could have used less:
chris@bobo:~$ cat myls.txt | less
OK, I think that's enough to absorb in one day. The trick is to play around a lot and use the commands frequently. After a while, they become second nature. Good luck!
No comments on this post so far. Leave a comment
Spooky UI story one day too late
Sunday, November 1st, 2009
Tags: user_interface.
I'm getting this spooky user interface story in one day too late for Halloween, but what the heck. I was talking to my mother the other day. As background, she's retired now, but she used computers for at least the last twenty years of work. She also spends a fair amount of time on the web (with firefox). So she hardly qualifies as a technophobe or the kind of user you simply can't coax into using your site.
Anyway, for one reason or another, she ended up a while ago with two different wish lists, one at Chapters and one at Amazon. This accident led over time to a bit of duplication, so she went to prune her Chapters wish list. But when she tried to do this, she found the user interface so complicated and frustrating that after a lot of fiddling and fussing and back clicking and scratching her head, she decided to delete her entire Chapters wish list. So now she just uses the Amazon one.
This is what happens when you make your user interface confusing. If you're responsible in one way or another for implementing a good user interface, you should find that a very spooooooky story.
No comments on this post so far. Leave a comment
Push creates new remote heads!
Monday, July 20th, 2009
Tags: mercurial, mercurial_error_message, version_control.
I'm putting this where I can easily find it again. I was working on the nyc-python website this afternoon. It's the first time I've used Mercurial for anything. I wish I could remember exactly how I backed myself into this particular corner, but I think it's that I added and committed changes before pulling from the repo. The other developer, Art, had in the meantime pushed other changes to the repo.
In any case, I tried to push to the repo and it told me:
pushing to ssh://[...] searching for changes abort: push creates new remote heads! (did you forget to merge? use push -f to force)
OK, no problem!
hg merge
Not no problem! The response:
abort: outstanding uncommitted changes
OK, I thought I had committed my changes, but whatever:
hg add hg commit
But then Mercurial told me that I had no changes to commit.
I found the explanation and a solution here.
The explanation: "Apparently you had uncommited changes (check with hg st -m) and so the merge aborts (it could come out with conflicts on files that have to be merged and have uncommitted changes)."
The solution:
$ cd .. $ hg clone nycpython nycpython-merge $ cd nycpython-merge/ $ hg merge $ hg ci $ cd .. $ cd nycpython $ hg pull ../nycpython-merge $ hg update $ hg pull $ hg push
And it worked!
No comments on this post so far. Leave a comment
A Note About Notes and Queries
Monday, June 29th, 2009
Tags: metablog.
Welcome to "Notes and Queries: An Occasional Blog."
Perhaps I should begin by saying a quick word about the title of this blog. I spent years in academia, sometimes toiling away over very obscure questions, especially relating to events in 4th Century B.C. Greece. One of the journals I came across while procrastinating was outside my field, but I could recognize kindred spirits in the authors' love of obscuranta. The journal was called "Notes and Queries."
When I left academia and started toiling away on computer programming, my work involved another sort of query, namely, of course, database queries. But I have fond memories of academia (though not only fond memories), and I thought "Notes and Queries" was pleasingly ambiguous title for the blog of someone making the transition between these two worlds.
I'll be writing here about programming stuff; I write about other subjects on my personal (group) blog.

No comments on this post so far. Leave a comment