其实这是个很小的事情,但是当你在setup.py和Python程序本身里都要 用到版本号的时候,为了“Don't repeat yourself”,还真的能逼死强迫症。

一般的版本标注方法

前面提到的setup.py说明文档里的例子都是这样的:

1
2
3
4
from distutils.core import setup
setup(name='foo',
      version='1.0',
      ...)

好吧其实就是直接写进去。但是我的foo程序要在信息页上显示自己的版本, 写在setup.py里的信息又不是程序的一部分,foo在运行时没办法再从setup.py 里拿到自己的元信息,怎么办呢?在程序里再写一遍?万一版本更新的时候我 不小心只改了一处呢?还能不能愉快地玩耍了?

你可能会说,那可以写在独立的配置文件里,放在setup.py和foo都能读到的 位置就好啦~当然可以,但是我有更干净的方法。

强迫症患者的标注方法

foo/version.py:

1
__version__ = '1.0'

setup.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import ast
from setuptools import setup

def get_version(fname):
    with open(fname) as f:
        source = f.read()
    module = ast.parse(source)
    for e in module.body:
        if isinstance(e, ast.Assign) and \
                len(e.targets) == 1 and \
                e.targets[0].id == '__version__' and \
                isinstance(e.value, ast.Str):
            return e.value.s
    raise RuntimeError('__version__ not found')

setup(name='foo',
      version=get_version('foo/version.py'),
      ...)

这里是将版本放在foo的代码里,然后在setup.py里通过ast模块将对应的 源码parse一遍得到抽象语法树,再从树里拿到__version__的值。这样发 布新版的时候只要改了源码里的版本,就可以高枕无忧了……

另一位强迫症患者的标注方法

foo/version.py:

1
2
import pkg_resources
__version__ = pkg_resources.get_distribution('foo').version

setup.py:

1
2
3
4
from setuptools import setup
setup(name='foo',
      version='1.0',
      ...)

上面说“在运行时没办法再从setup.py里拿到自己的元信息”有点坑的意味——其实是 可以从别的地方拿的。这里刚好和上面的第一个方法倒过来,程序里用到版本的时 候再用pkg_resources去查。版本信息同样只位于一个位置,版本有变化的时 候只需要改setup.py就可以了。

不幸的是,呃,可能很多同学已经想到了,这个方法有个硬伤:foo中但凡引用了 __version__的代码,就只有在foo被正确安装之后才能正确执行,不然的话——例 如在单元测试中——就只能monkey patch一下了。

结论

我是倾向于用第一种方法的。第二种方法的硬伤能不能接受?其实见仁见智啦……我 会觉得每次引用一个变量的时候都要小心翼翼比较烦人而且容易出错,用第一种方 法在setup.py里一次性加一段代码其实会更干净。