• Python tests with doctest and unittest

    When it comes to tests, doctest is a great simple module to write tests for your application. However it is pretty basic and does not have any extended features like, for example, centralized unit tests. If you have multiple modules with doctests (and you probably do) you most likely want to be able to run all doctests recursively from one place. That’s where unittest comes in.

    Let’s assume we store modules in the lib/ directory:

    $ ls lib/
    __init__.py bar.py foo.py
    

    Here are the contents of foo.py and bar.py respectfully:

    def foo():
        """
        >>> foo()
        False
        """
        return False
    
    def bar():
        """
        >>> bar()
        True
        """
        return True
    
    def baz():
        """
        >>> baz()
        False
        """
        return False
    

    Now, to run all tests we need a wrapper script. Let’s call it: runtests.py:

    #!/usr/bin/env python
    
    import unittest
    import doctest
    import os
    
    files = []
    root_dir = 'lib/'
    
    for root, _, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename == '__init__.py' or filename[-3:] != '.py':
                continue
            f = os.path.join(root, filename)
            f = f.replace('/', '.')
            f = f[:-3]
            files.append(f)
    
    suite = unittest.TestSuite()
    for module in files:
        suite.addTest(doctest.DocTestSuite(module))
    unittest.TextTestRunner(verbosity=1).run(suite)
    

    This approach invokes the doctest.DocTestSuite method, which converts doctests strings into unittest suites. Time to run our tests:

    $ chmod +x runtests.py
    $ ./runtests.py
    ...
    ----------------------------------------------------------------------
    Ran 3 tests in 0.008s
    
    OK
    

    And just to be sure that approach actually works, let’s make one of the tests fail:

    $ ./runtests.py
    .F.
    ======================================================================
    FAIL: baz (lib.bar)
    Doctest: lib.bar.baz
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/usr/lib/python2.7/doctest.py", line 2201, in runTest
        raise self.failureException(self.format_failure(new.getvalue()))
    AssertionError: Failed doctest test for lib.bar.baz
      File "/home/rosipov/unitdoc/lib/bar.py", line 8, in baz
    
    ----------------------------------------------------------------------
    File "/home/rosipov/unitdoc/lib/bar.py", line 10, in lib.bar.baz
    Failed example:
        baz()
    Expected:
        True
    Got:
        False
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.009s
    
    FAILED (failures=1)
    
  • pygame.font not found

    I had an issue with pygame not being able to find a dependency for the font module. After quite a time-consuming search the missing package name was libsdl-ttf2.0-dev.

    Hope this helps someone.

  • Use vimdiff as git mergetool

    Using vimdiff as a git mergetool can be pretty confusing - multiple windows and little explanation. This is a short tutorial which explains basic usage, and what the LOCAL, BASE, and REMOTE keywords mean. This implies that you have at least a little bit of basic vim knowledge (how to move, save, and switch between split windows). If you don’t, there’s a short article for you: Using vim for writing code. Some basic understanding of git and branching is required as well, obviously.

    Git config

    Prior to doing anything, you need to know how to set vimdiff as a git mergetool. That being said:

    git config merge.tool vimdiff
    git config merge.conflictstyle diff3
    git config mergetool.prompt false
    

    This will set git as the default merge tool, will display a common ancestor while merging, and will disable the prompt to open the vimdiff.

    Creating merge conflict

    Let’s create a test situation. You are free to skip this part or you can work along with the tutorial.

    mkdir zoo
    cd zoo
    git init
    vi animals.txt
    

    Let’s add some animals:

    cat
    dog
    octopus
    octocat
    

    Save the file.

    git add animals.txt
    git commit -m "Initial commit"
    git branch octodog
    git checkout octodog
    vi animals.txt  # let's change octopus to octodog
    git add animals.txt
    git commit -m "Replace octopus with an octodog"
    git checkout master
    vi animals.txt  # let's change octopus to octoman
    git add animals.txt
    git commit -m "Replace octopus with an octoman"
    git merge octodog  # merge octodog into master
    

    That’s where we get a merge error:

    Auto-merging animals.txt
    CONFLICT (content): Merge conflict in animals.txt
    Automatic merge failed; fix conflicts and then commit the result.
    

    Resolving merge conflict with vimdiff

    Let’s resolve the conflict:

    git mergetool
    

    Three-way merge using vimdiff. Local changes are in top left, followed by a common ancestor, and branch `octodog` in the top right corner. Resulting file is at the bottom.

    This looks terrifying at first, but let me explain what is going on.

    From left to right, top to the bottom:

    LOCAL – this is file from the current branch BASE – common ancestor, how file looked before both changes REMOTE – file you are merging into your branch MERGED – merge result, this is what gets saved in the repo

    Let’s assume that we want to keep the “octodog” change (from REMOTE). For that, move to the MERGED file (Ctrl + w, j), move your cursor to a merge conflict area and then:

    :diffget RE
    

    This gets the corresponding change from REMOTE and puts it in MERGED file. You can also:

    :diffg RE  " get from REMOTE
    :diffg BA  " get from BASE
    :diffg LO  " get from LOCAL
    

    Save the file and quit (a fast way to write and quit multiple files is :wqa).

    Run git commit and you are all set!

    If you’d like to get even better about using Vim, I wrote a book about it: Mastering Vim. I’m pretty proud of how it turned out, and I hope you like it too.

  • Git pretty log output

    This alias has been around the web for quite some time, but it does look fantastic indeed.

    An output of `git pretty-log` alias.

    To add the alias git pretty-log, execute the following command (join string prior to executing):

    git config alias.pretty-log 'log --graph --pretty=format:"%Cred%h%Creset
    -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset"'
    
  • Download gists from prompt

    I wrote a little script to download gists from the command prompt.

    Generate your Github API Token under Settings -> Applications, change it within a script, and then:

    chmod +x shgist.py
    mv shgist.py ~/bin/shgist
    

    Where ~/bin is a directory in your path. Now you can use it as shgist file to quickly download your gists (Gist on Github).

    #!/usr/bin/env python
    
    # Ruslan Osipov
    # Usage: shgist keywords
    # Description: Gists downloader
    
    import urllib
    import urllib2
    import sys
    import json
    
    token = 'Personal API Access Token'  # Github Settings -> Applications
    
    class Gist:
        def __init__(self, token):
            """
            token -- str, github token
            """
            self.token = token
            self.url = 'https://api.github.com'
    
        def find_by_name(self, keywords):
            """
            keywords -- list of strings
            """
            gists, urls = self._get_gists()
            for i, gist in enumerate(gists):
                for keyword in keywords:
                    if keyword not in gist:
                        del gists[i]
                        del urls[i]
                        break
            if len(gists) == 0:
                print "Sorry, no gists matching your description"
                return
            if len(gists) == 1:
                self._download_gist(gists[0], urls[0])
                return
            for i, gist in enumerate(gists):
                print i, gist
            while True:
                num = raw_input("Gist number, 'q' to quit: ")
                if num == 'q':
                    print "Quiting..."
                    return
                try:
                    num = int(num)
                    if 0 <= num < len(gists):
                        break
                    print "Number should be within specified range"
                except:
                    print "Only integers or 'q' are allowed"
            self._download_gist(gists[num], urls[num])
    
        def _download_gist(self, name, url):
            """
            name -- str, filename
            url -- str, raw gist url
            """
            print "Downloading %s..." % name
            gist = self._send_get_request(url)
            open(name, 'wb').write(gist)
    
        def _get_gists(self):
            """
            Returns 2 lists which should be treated as ordered dict
            """
            url = '/gists'
            response = self._send_get_request(self.url + url)
            response = json.loads(response)
            gists, urls = [], []
            for gist in response:
                for name, meta in gist['files'].items():
                    gists.append(name)
                    urls.append(meta['raw_url'])
            return gists, urls
    
        def _send_get_request(self, url):
            """
            url -- str
            """
            headers = {
                    'Authorization': 'token ' + self.token
                    }
            request = urllib2.Request(url, headers=headers)
            response = urllib2.urlopen(request)
            return response.read()
    
    argv = sys.argv[1:]
    if not len(argv):
        print "Usage: shgist keywords"
        sys.exit(0)
    
    gist = Gist(token)
    gist.find_by_name(argv)