Revision 866bb9c1

b/devflow/autopkg.py
37 37
from sh import mktemp, cd, rm, git_dch, python
38 38
from optparse import OptionParser
39 39

  
40
from devflow.versioning import (get_python_version,
41
                                debian_version_from_python_version)
42

  
40 43
try:
41 44
    from colors import red, green
42 45
except ImportError:
......
55 58

  
56 59
def main():
57 60
    from devflow.version import __version__
58

  
59 61
    parser = OptionParser(usage="usage: %prog [options] mode",
60
                          version="%prog - devflow %s" % __version__)
62
                          version="devflow %s" % __version__)
61 63
    parser.add_option("-k", "--keep-repo",
62 64
                      action="store_true",
63 65
                      dest="keep_repo",
......
79 81

  
80 82
    (options, args) = parser.parse_args()
81 83

  
82
    mode = args[0]
84
    try:
85
        mode = args[0]
86
    except IndexError:
87
        raise ValueError("Mode argument is mandatory. Usage: %s"
88
                         % parser.usage)
83 89
    if mode not in AVAILABLE_MODES:
84 90
        raise ValueError(red("Invalid argument! Mode must be one: %s"
85 91
                         % ", ".join(AVAILABLE_MODES)))
......
118 124
        repo.references[debian_branch]
119 125
    except IndexError:
120 126
        # Branch does not exist
121
        # FIXME: remove hard-coded strings..
122
        if branch == "debian":
123
            repo.git.branch("--track", debian_branch, "origin/debian")
124
        else:
125
            repo.git.branch("--track", debian_branch, "origin/debian-develop")
127
        repo.git.branch("--track", debian_branch, "origin/" + debian_branch)
126 128

  
127 129
    repo.git.checkout(debian_branch)
128 130
    print_green("Changed to branch '%s'" % debian_branch)
......
131 133
    print_green("Merged branch '%s' into '%s'" % (branch, debian_branch))
132 134

  
133 135
    cd(repo_dir)
134
    version = python(repo_dir + "/devflow/version.py", "debian").strip()
135
    print_green("The new debian version will be: '%s'" % version)
136
    python_version = get_python_version()
137
    debian_version = debian_version_from_python_version(python_version)
138
    print_green("The new debian version will be: '%s'" % debian_version)
136 139

  
137 140
    dch = git_dch("--debian-branch=%s" % debian_branch,
138 141
            "--git-author",
139 142
            "--ignore-regex=\".*\"",
140 143
            "--multimaint-merge",
141 144
            "--since=HEAD",
142
            "--new-version=%s" % version)
145
            "--new-version=%s" % debian_version)
143 146
    print_green("Successfully ran '%s'" % " ".join(dch.cmd))
144 147

  
145
    os.system("vim debian/changelog")
146 148
    repo.git.add("debian/changelog")
147 149

  
148 150
    if mode == "release":
151
        os.system("vim debian/changelog")
152
        repo.git.add("debian/changelog")
149 153
        repo.git.commit("-s", "-a", "-m", "Bump new upstream version")
150
        if branch == "master":
151
            repo.git.tag("debian/" + version)
154
        python_tag = python_version
155
        debian_tag = "debian/" + python_tag
156
        repo.git.tag(debian_tag)
157
        repo.git.tag(python_tag, branch)
152 158

  
153 159
    for package in PACKAGES:
154 160
        # python setup.py should run in its directory
155 161
        cd(package)
156 162
        package_dir = repo_dir + "/" + package
157 163
        res = python(package_dir + "/setup.py", "sdist", _out=sys.stdout)
158
        cd("../")
159 164
        print res.stdout
165
        if package != ".":
166
            cd("../")
160 167

  
161 168
    # Add version.py files to repo
162 169
    os.system("grep \"__version_vcs\" -r . -l -I | xargs git add -f")
......
167 174
        print_green("Created directory '%s' to store the .deb files." %
168 175
                     build_dir)
169 176

  
177
    cd(repo_dir)
170 178
    os.system("git-buildpackage --git-export-dir=%s --git-upstream-branch=%s"
171 179
              " --git-debian-branch=%s --git-export=INDEX --git-ignore-new -sa"
172 180
              % (build_dir, branch, debian_branch))
......
178 186
        print_green("Repository dir '%s'" % repo_dir)
179 187

  
180 188
    print_green("Completed. Version '%s', build area: '%s'"
181
                % (version, build_dir))
189
                % (debian_version, build_dir))
190

  
191
    if mode == "release":
192
        TAG_MSG = "Tagged branch %s with tag %s\n"
193
        print_green(TAG_MSG % (branch, python_tag))
194
        print_green(TAG_MSG % (debian_branch, debian_tag))
195

  
196
        UPDATE_MSG = "To update repository %s, go to %s, and run the"\
197
                     " following commands:\n" + "git_push origin %s\n" * 3
198

  
199
        origin_url = repo.remotes['origin'].url
200
        remote_url = original_repo.remotes['origin'].url
201

  
202
        print_green(UPDATE_MSG % (origin_url, repo_dir, debian_branch,
203
                    debian_tag, python_tag))
204
        print_green(UPDATE_MSG % (remote_url, original_repo.working_dir,
205
                    debian_branch, debian_tag, python_tag))
182 206

  
183 207

  
184 208
if __name__ == "__main__":
/dev/null
1
#!/usr/bin/env python
2
#
3
# Copyright 2012 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35
#
36
#
37

  
38
"""Unit Tests for devflow.versioning
39

  
40
Provides unit tests for module devflow.versioning,
41
for automatic generation of version strings.
42

  
43
"""
44

  
45
import os
46
import unittest
47
from pkg_resources import parse_version
48
from versioning import debian_version_from_python_version
49

  
50

  
51
class DebianVersionObject(object):
52
    """Object representing a Debian Version."""
53
    def __init__(self, pyver):
54
        self.version = debian_version_from_python_version(pyver)
55

  
56
    def __str__(self):
57
        return self.version
58

  
59

  
60
def debian_compare_versions(a, op, b):
61
    i = os.system("dpkg --compare-versions %s %s %s" % (a, op, b))
62
    return i == 0
63

  
64
# Set ordering between DebianVersionObject objects, by adding
65
# debian_compare_versions
66
for op in ["lt", "le", "eq", "ne", "gt", "ge"]:
67
    def gen(op):
68
        def operator_func(self, other):
69
            return debian_compare_versions(self.version, op, other.version)
70
        return operator_func
71
    setattr(DebianVersionObject, "__%s__" % op, gen(op))
72

  
73

  
74
def _random_commit():
75
    import random
76
    import string
77
    return "".join(random.choice(string.hexdigits) for n in xrange(8)).lower()
78

  
79

  
80
# Add a random commit number at the end of snapshot versions
81
def version_with_commit(parse_func, v):
82
    if "_" in v:
83
        return parse_func(v + "_" + _random_commit())
84
    else:
85
        return parse_func(v)
86

  
87
V = lambda v: version_with_commit(parse_version, v)
88
D = lambda v: version_with_commit(DebianVersionObject, v)
89

  
90

  
91
class TestVersionFunctions(unittest.TestCase):
92
    def setUp(self):
93
        self.version_orderings = (
94
            ("0.14next", ">", "0.14"),
95
            ("0.14next", ">", "0.14rc7"),
96
            ("0.14next", "<", "0.14.1"),
97
            ("0.14rc6", "<", "0.14"),
98
            ("0.14.2rc6", ">", "0.14.1"),
99
            ("0.14next_150", "<", "0.14next"),
100
            ("0.14.1next_150", "<", "0.14.1next"),
101
            ("0.14.1_149", "<", "0.14.1"),
102
            ("0.14.1_149", "<", "0.14.1_150"),
103
            ("0.13next_102", "<", "0.13next"),
104
            ("0.13next", "<", "0.14rc5_120"),
105
            ("0.14rc3_120", "<", "0.14rc3"),
106
            # The following test fails, but versioning.python_version
107
            # will never try to produce such a version:
108
            # ("0.14rc3", "<", "0.14_1"),
109
            ("0.14_120", "<", "0.14"),
110
            ("0.14", "<", "0.14next_20"),
111
            ("0.14next_20", "<", "0.14next"),
112
        )
113

  
114
    def test_python_versions(self):
115
        for a, op, b in self.version_orderings:
116
            res = compare(V, a, op, b)
117
            self.assertTrue(res, "Python version: %s %s %s"
118
                                 " is not True" % (a, op, b))
119

  
120
    def test_debian_versions(self):
121
        for a, op, b in self.version_orderings:
122
            res = compare(D, a, op, b)
123
            self.assertTrue(res, "Debian version %s %s %s"
124
                                 " is not True" % (a, op, b))
125

  
126

  
127
def compare(function, a, op, b):
128
    import operator
129
    str_to_op = {"<": operator.lt,
130
            "<=": operator.le,
131
            "==": operator.eq,
132
            ">": operator.gt,
133
            ">=": operator.ge}
134
    try:
135
        return str_to_op[op](function(a), function(b))
136
    except KeyError:
137
        raise ValueError("Unknown operator '%s'" % op)
138

  
139
if __name__ == '__main__':
140
    unittest.main()
b/devflow/versioning.py
83 83
    elif len(parents) == 2:
84 84
        if cur_br_name.startswith("debian-") or cur_br_name == "debian":
85 85
            pr1, pr2 = parents
86
            return short_id(pr1) + "~" + short_id(pr2)
86
            return short_id(pr1) + "-" + short_id(pr2)
87 87
        else:
88 88
            return short_id(commit)
89 89
    else:
......
107 107
        branch = repo.head.reference
108 108
        revid = get_commit_id(branch.commit, branch)
109 109
        revno = len(list(repo.iter_commits()))
110
        desc = repo.git.describe("--tags")
111 110
        toplevel = repo.working_dir
112 111
    except git.InvalidGitRepositoryError:
113 112
        log.error("Could not retrieve git information. " +
......
115 114
        return None
116 115

  
117 116
    info = namedtuple("vcs_info", ["branch", "revid", "revno",
118
                                   "desc", "toplevel"])
117
                                   "toplevel"])
119 118

  
120
    return info(branch=branch.name, revid=revid, revno=revno, desc=desc,
119
    return info(branch=branch.name, revid=revid, revno=revno,
121 120
                toplevel=toplevel)
122 121

  
123 122

  
......
137 136
    try:
138 137
        mode = os.environ["GITFLOW_BUILD_MODE"]
139 138
        assert mode == "release" or mode == "snapshot"
140
    except (KeyError, AssertionError):
141
        raise ValueError("GITFLOW_BUILD_MODE environment variable must be "
142
                         "'release' or 'snapshot'")
139
    except KeyError:
140
        raise ValueError("GITFLOW_BUILD_MODE environment variable is not set."
141
                         " Set this variable to 'release' or 'snapshot'")
142
    except AssertionError:
143
        raise ValueError("GITFLOW_BUILD_MODE environment variable must be"
144
                         " 'release' or 'snapshot'")
143 145
    return mode
144 146

  
145 147

  
......
390 392
    return pyver.replace("_", "~").replace("rc", "~rc") + "-1"
391 393

  
392 394

  
395
def get_python_version():
396
    v = vcs_info()
397
    b = base_version(v)
398
    mode = build_mode()
399
    return python_version(b, v, mode)
400

  
401

  
393 402
def debian_version(base_version, vcs_info, mode):
394 403
    p = python_version(base_version, vcs_info, mode)
395 404
    return debian_version_from_python_version(p)
396 405

  
397 406

  
407
def get_debian_version():
408
    v = vcs_info()
409
    b = base_version(v)
410
    mode = build_mode()
411
    return debian_version(b, v, mode)
412

  
413

  
398 414
def user_info():
399 415
    import getpass
400 416
    import socket
......
410 426

  
411 427
    """
412 428

  
429
    paths = [root] + module.split(".") + ["%s.py" % name]
430
    module_filename = os.path.join(*paths)
431

  
413 432
    v = vcs_info()
414 433
    if not v:
415 434
        # Return early if not in development environment
435
        log.error("Can not compute version outside of a git repository."
436
                  " Will not update %s version file" % module_filename)
416 437
        return
417 438
    b = base_version(v)
418 439
    mode = build_mode()
419
    paths = [root] + module.split(".") + ["%s.py" % name]
420
    module_filename = os.path.join(*paths)
421 440
    version = python_version(b, v, mode)
422 441
    content = """
423 442
__version__ = "%(version)s"
424 443
__version_info__ = %(version_info)s
425 444
__version_vcs_info__ = %(vcs_info)s
426 445
__version_user_info__ = "%(user_info)s"
427
    """ % dict(version=version, version_info=version.split("."),
446
""" % dict(version=version, version_info=version.split("."),
428 447
               vcs_info=pprint.PrettyPrinter().pformat(dict(v._asdict())),
429 448
               user_info=user_info())
430 449

  
431 450
    module_file = file(module_filename, "w+")
432 451
    module_file.write(content)
433 452
    module_file.close()
453
    return module_filename
434 454

  
435 455

  
436
if __name__ == "__main__":
456
def main():
437 457
    v = vcs_info()
438 458
    b = base_version(v)
439 459
    mode = build_mode()
......
448 468
        print python_version(b, v, mode)
449 469
    elif arg == "debian":
450 470
        print debian_version(b, v, mode)
471

  
472
if __name__ == "__main__":
473
    sys.exit(main())
b/setup.py
1
# Copyright 2011 GRNET S.A. All rights reserved.
1
# Copyright 2012 GRNET S.A. All rights reserved.
2 2
#
3 3
# Redistribution and use in source and binary forms, with or
4 4
# without modification, are permitted provided that the following
......
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33
#
34

  
35 34
import distribute_setup
36 35
distribute_setup.use_setuptools()
37 36

  
......
43 42
from setuptools import setup, find_packages
44 43

  
45 44
HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
46

  
47 45
try:
46
    from devflow import versioning
48 47
    # use devflow to update the version file
49
    from devflow.versioning import update_version
50
    update_version('devflow', 'version', HERE)
48
    versioning.update_version('devflow', 'version', HERE)
51 49
except ImportError:
52
    raise RuntimeError("devflow is a build dependency")
50
    version_fpath = os.path.join(HERE, 'devflow', 'version.py')
51
    sys.stdout.write("WARNING: Can not update version because `devflow` is"
52
                    " not installed. Please make sure to manually"
53
                    " update version file %s" % version_fpath)
53 54

  
54 55
from devflow.version import __version__
55 56

  
......
67 68

  
68 69
# Package requirements
69 70
INSTALL_REQUIRES = [
70
    'git'
71
    'gitpython'
71 72
]
72 73

  
73 74
# Provided as an attribute, so you can append to these instead
......
141 142
                        new_package = package + "." + name
142 143
                    stack.append((fn, "", new_package, False))
143 144
                else:
144
                    stack.append((fn, prefix + name + "/", package, only_in_packages))
145
                    stack.append((fn, prefix + name + "/", package,
146
                                  only_in_packages))
145 147
            elif package or not only_in_packages:
146 148
                # is a file
147 149
                bad_name = False
......
156 158
                        break
157 159
                if bad_name:
158 160
                    continue
159
                out.setdefault(package, []).append(prefix+name)
161
                out.setdefault(package, []).append(prefix + name)
160 162
    return out
161 163

  
162 164
setup(
163
    name = 'devflow',
164
    version = VERSION,
165
    license = 'BSD',
166
    url = 'http://www.synnefo.ogr/',
167
    description = SHORT_DESCRIPTION,
168
    long_description=README + '\n\n' +  CHANGES,
169
    classifiers = CLASSIFIERS,
170

  
171
    author = 'GRNET dev team',
172
    author_email = 'okeanos-dev@lists.grnet.gr',
173
    maintainer = 'GRNET dev team',
174
    maintainer_email = 'okeanos-dev@lists.grnet.gr',
175

  
176
    packages = PACKAGES,
177
    package_dir= {'': PACKAGES_ROOT},
178
    include_package_data = True,
179
    package_data = find_package_data('.'),
180
    zip_safe = False,
181

  
182
    install_requires = INSTALL_REQUIRES,
183

  
184
    entry_points = {
165
    name='devflow',
166
    version=VERSION,
167
    license='BSD',
168
    url='http://www.synnefo.ogr/',
169
    description=SHORT_DESCRIPTION,
170
    long_description=README + '\n\n' + CHANGES,
171
    classifiers=CLASSIFIERS,
172

  
173
    author='GRNET dev team',
174
    author_email='okeanos-dev@lists.grnet.gr',
175
    maintainer='GRNET dev team',
176
    maintainer_email='okeanos-dev@lists.grnet.gr',
177

  
178
    packages=PACKAGES,
179
    package_dir={'': PACKAGES_ROOT},
180
    include_package_data=True,
181
    package_data=find_package_data('.'),
182
    zip_safe=False,
183

  
184
    install_requires=INSTALL_REQUIRES,
185

  
186
    entry_points={
185 187
     'console_scripts': [
186
         'devflow-version = devflow.versioning:main',
188
         'devflow-version=devflow.versioning:main',
189
         'devflow-autopkg=devflow.autopkg:main',
187 190
         ],
188 191
      },
189 192
)
b/tests/test_versioning.py
1
#!/usr/bin/env python
2
#
3
# Copyright 2012 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35
#
36
#
37

  
38
"""Unit Tests for devflow.versioning
39

  
40
Provides unit tests for module devflow.versioning,
41
for automatic generation of version strings.
42

  
43
"""
44

  
45
import os
46
import unittest
47
from pkg_resources import parse_version
48
from devflow.versioning import debian_version_from_python_version
49

  
50

  
51
class DebianVersionObject(object):
52
    """Object representing a Debian Version."""
53
    def __init__(self, pyver):
54
        self.version = debian_version_from_python_version(pyver)
55

  
56
    def __str__(self):
57
        return self.version
58

  
59

  
60
def debian_compare_versions(a, op, b):
61
    i = os.system("dpkg --compare-versions %s %s %s" % (a, op, b))
62
    return i == 0
63

  
64
# Set ordering between DebianVersionObject objects, by adding
65
# debian_compare_versions
66
for op in ["lt", "le", "eq", "ne", "gt", "ge"]:
67
    def gen(op):
68
        def operator_func(self, other):
69
            return debian_compare_versions(self.version, op, other.version)
70
        return operator_func
71
    setattr(DebianVersionObject, "__%s__" % op, gen(op))
72

  
73

  
74
def _random_commit():
75
    import random
76
    import string
77
    return "".join(random.choice(string.hexdigits) for n in xrange(8)).lower()
78

  
79

  
80
# Add a random commit number at the end of snapshot versions
81
def version_with_commit(parse_func, v):
82
    if "_" in v:
83
        return parse_func(v + "_" + _random_commit())
84
    else:
85
        return parse_func(v)
86

  
87
V = lambda v: version_with_commit(parse_version, v)
88
D = lambda v: version_with_commit(DebianVersionObject, v)
89

  
90

  
91
class TestVersionFunctions(unittest.TestCase):
92
    def setUp(self):
93
        self.version_orderings = (
94
            ("0.14next", ">", "0.14"),
95
            ("0.14next", ">", "0.14rc7"),
96
            ("0.14next", "<", "0.14.1"),
97
            ("0.14rc6", "<", "0.14"),
98
            ("0.14.2rc6", ">", "0.14.1"),
99
            ("0.14next_150", "<", "0.14next"),
100
            ("0.14.1next_150", "<", "0.14.1next"),
101
            ("0.14.1_149", "<", "0.14.1"),
102
            ("0.14.1_149", "<", "0.14.1_150"),
103
            ("0.13next_102", "<", "0.13next"),
104
            ("0.13next", "<", "0.14rc5_120"),
105
            ("0.14rc3_120", "<", "0.14rc3"),
106
            # The following test fails, but versioning.python_version
107
            # will never try to produce such a version:
108
            # ("0.14rc3", "<", "0.14_1"),
109
            ("0.14_120", "<", "0.14"),
110
            ("0.14", "<", "0.14next_20"),
111
            ("0.14next_20", "<", "0.14next"),
112
        )
113

  
114
    def test_python_versions(self):
115
        for a, op, b in self.version_orderings:
116
            res = compare(V, a, op, b)
117
            self.assertTrue(res, "Python version: %s %s %s"
118
                                 " is not True" % (a, op, b))
119

  
120
    def test_debian_versions(self):
121
        for a, op, b in self.version_orderings:
122
            res = compare(D, a, op, b)
123
            self.assertTrue(res, "Debian version %s %s %s"
124
                                 " is not True" % (a, op, b))
125

  
126

  
127
def compare(function, a, op, b):
128
    import operator
129
    str_to_op = {"<": operator.lt,
130
            "<=": operator.le,
131
            "==": operator.eq,
132
            ">": operator.gt,
133
            ">=": operator.ge}
134
    try:
135
        return str_to_op[op](function(a), function(b))
136
    except KeyError:
137
        raise ValueError("Unknown operator '%s'" % op)
138

  
139
if __name__ == '__main__':
140
    unittest.main()
b/version
1
0.1next

Also available in: Unified diff