Welcome, guest | Sign In | My Account | Store | Cart

The script shows how the standard Distutils module can be easily customized to add new features. In this example, a typical "setup.py" file is configured to extract revision information from a Subversion (revision control repository) database and use the information to set the version of the distribution.

Python, 88 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# setup.py -- customized Python Disutils distribution / installation script

# Written By: Chadwick Stryker

from distutils.core import setup, Distribution
from os.path import join as pjoin
import os, sys

def committed_rev():
    """
    Fetches the last committed rev to the repository and returns it.  This uses
    the pysvn module.  If anything goes wrong it sets the revision to zero.
    """
    try:
        import pysvn
        client = pysvn.Client()
        revs = []
        # search through the directory tree, starting at setup.py's directory
        for root, dirs, files in os.walk(sys.path[0]):
            # don't search through the .svn directories
            if '.svn' in dirs:
                dirs.remove('.svn')
            # get the revision info for each file, one at a time
            for f in files:
                try:
                    # try to get the SVN info. When checking a non working-copy
                    # directory, an exception will be raised
                    entry = client.info(pjoin(root,f))
                    # verify that the commit revision for the file is a number
                    if entry.commit_revision.kind != \
                        pysvn.opt_revision_kind.number:
                        raise
                    # if we made it this far (i.e. no exception raised), 
                    # remember the revision of the file for later
                    revs.append(entry.commit_revision.number)
                # otherwise, if client.info() fails or an exception is raised, 
                # then skip this file
                except:
                    pass
        # return the highest revision number of any file found in our search
        return max(revs)
    # if an unhandled exception occurs, then abort the search process and 
    # return a rev of zero. An example would be an ImportError generated if
    # the pysvn module is not installed on the system
    except Exception, msg:
        print msg,
        print '-- aborting search for subversion repository revision number'
        return 0

class svnDistribution(Distribution):
    """
    This subclass of the Distribution class is Subversion aware and
    searches to find the overall revision of the current working copy.
    """
    def __init__(self, attrs):
        # this does most of the work...
        build_num = committed_rev()
        if build_num:
            # if there is SVN revision data, assign the new version number
            attrs['version'] = '%i' % build_num
            try:
                # then try to create a user specified version file
                filename, format  = attrs['version_file']
                file(filename,'w').write(format % build_num)
            # in case a 'version_file' attribute was not set, do nothing
            except KeyError:
                pass
        # the parent class does not know about 'version_file', so delete it
        del attrs['version_file']
        Distribution.__init__(self, attrs)

setup(  name='example',
        description='example python module',
        author='Chad Stryker',
        author_email="example@example.net",
        py_modules=['example'],
        
        # the following attributes are used with the version number feature...
        
        # use this version ID if .svn data cannot be found
        version='SVN data unavailable', 
        distclass = svnDistribution,
        # the version_file attribute is understood by the svnDistribution class
        # and provides a way for the version information to be accessible by 
        # the module after it is installed
        version_file = ('_version.py', \
            '#This file is generated automatically\nversion = "%i"')
        )

Being a user of both the Python programming language and the Subversion revision control system, I looked for a way to have my distribution package creation process automatically assign the distribution revision number to that of the latest committed revision in the Subversion repository. My original approach dynamically created a Distutils setup.py file from a Windows batch file. The batch file would run another Python script to get the revision information out of the repository and dynamically create the setup.py file to add the revision information. This process always seemed messy and complicated, so I looked for a cleaner approach.

This example demonstrates my eventual solution which confines all of the customization to the setup.py file. The most interesting part of this approach is the modification of the standard Distutils distribution class by creating a new distribution subclass. The new subclass performs the revision control assignment in the subclass' constructor and then calls the constructor for the original parent class. Distutils offers a convenient way to specify the new subclass [1]. In order to make this approach work cleanly, the new subclass has to handle the case were no Subversion repository can be found. This will happen if someone tries to create a package from something other than Subversion repository's working copy. In this case, I preferred that the distribution creation process would continue but just omit the revision information.

[1] http://docs.python.org/dist/module-distutils.core.html