如何快速构建一个 Python 模块项目(开源方案)

这里只是用一个 Hello World 级别的模板类型项目,来说明下如何快速构建起来一个 Python 项目。且是在开源场景下发布,如果是闭源场景,再单独写一篇文章类似说明。

先看结果

我们最终要完成的就是一个 Python 模块安装包。这里“完成”的定义是:实现功能、通过测试、发布到网上供别人使用。

比如:我们最后发布的是一个叫 onepiece 的 Python 库。

因为这里是允许开源的场景,所以就可以直接发布到 PyPI https://pypi.python.org/pypi 了,如图:

../../../_images/onepiece.png

然后就可以直接用了,比如用 pip 方式安装:

$ pip install onepiece

onepiece 库中演示模块的功能很简单,就是打印一个字符串“One Piece”就完事,如下:

>>> from onepiece.example import hello_world
>>> hello_world()
>>> 'One Piece'

项目的源码直接查看 GitHub:https://github.com/akun/onepiece

下面我们来看下,如何快速构建这么一个 Python 模块项目。

first commit

直接用项目模板来初始化项目,后续再单独写一篇文章,详细解释由模板生成的模块项目中各个文件的作用。因为用了模板,这里所谓的“快速构建”模板的历史积累起到很大的作用,我们要遵循 DRY(Don’t Repeat Yourself)这个原则。

安装模板工具

你得先安装一个模板工具,后续会用到,如下:

$ pip install cookiecutter

或者如果你用的是 Ubuntu 的话,也可以:

$ apt-get install python-cookiecutter

初始化项目

然后,就可以用现成的 Python 项目模板初始化了,比如:

$ cookiecutter https://github.com/akun/aproject.git  # 按提示输入内容即可
$ cd onepiece
$ virtualenv onepiece_venv
$ source onepiece_venv/bin/activate
$ make

初始化 Git 本地仓库

$ git init
$ git add .
$ git commit  # 比如日志是:chore: init project

推送到 Git 远程仓库

我们直接用 GitHub,直接在上面新建个项目,就叫 onepiece,然后把 Git 本地仓库推送到 Git 远程仓库:

$ git remote add origin https://github.com/akun/onepiece.git
$ git push origin master

DONE

然后从 GitHub 上查看第一次提交的结果吧:https://github.com/akun/onepiece

就这样 first commit 就包含了一个完整的初始项目了,然而还没“完成”。

做好配置管理工作

下列原则可以参考下:

  • 用项目模板协助初始化常见的配置管理;
  • 代码未动,CI/CD 先行:因为这里是允许开源的场景,我们选用 Travis-CI 这个服务;
  • 优先考虑安装部署脚本:立马发布一个空壳项目;
  • 尽可能多的记录软件开发中产生的常见行为,参考: 《 如何理解版本控制系统 》。

实现功能并测试

因为是个示例,所以这里实现的功能就很简单了,就是打印一个字符串,代码如下:

#!/usr/bin/env python
# coding=utf-8


def hello_world():
    return 'One Piece'

同样,测试代码也很简单,代码如下:

#!/usr/bin/env python
# coding=utf-8


from unittest import TestCase

from onepiece.example import hello_world


class HelloWordTestCase(TestCase):

    def test_hello_world(self):
        self.assertEqual('One Piece', hello_world())

测试

独立一节来说,就是为了说明构建中测试是必须的环节,在这里,测试也很简单:

$ make test

可以看到所有测试用例在 Python 2 和 Python 3 下都通过,测试覆盖率是 100%,如下:

GLOB sdist-make: /home/kun/projects/pm/pm/onepiece/setup.py
py27 inst-nodeps: /home/kun/projects/pm/pm/onepiece/.tox/dist/onepiece-0.1.0.zip
py27 installed: coverage==4.5.1,future==0.16.0,httpretty==0.8.14,nose==1.3.7,onepiece==0.1.0,pkg-resources==0.0.0
py27 runtests: PYTHONHASHSEED='1283052189'
py27 runtests: commands[0] | nosetests -c nose.cfg
...E..
======================================================================
ERROR: test_do_print_example (tests.test_main.MainTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/kun/projects/pm/pm/onepiece/tests/test_main.py", line 16, in test_do_print_example
    text = main.do_print_example()
  File "/home/kun/projects/pm/pm/onepiece/onepiece/main.py", line 15, in do_print_example
    print(text)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)

Name                   Stmts   Miss  Cover
------------------------------------------
onepiece/__init__.py       1      0   100%
onepiece/example.py        2      0   100%
onepiece/main.py          22      0   100%
------------------------------------------
TOTAL                     25      0   100%
----------------------------------------------------------------------
Ran 6 tests in 0.097s

FAILED (errors=1)
ERROR: InvocationError: '/home/kun/projects/pm/pm/onepiece/.tox/py27/bin/nosetests -c nose.cfg'
py3 inst-nodeps: /home/kun/projects/pm/pm/onepiece/.tox/dist/onepiece-0.1.0.zip
py3 installed: coverage==4.5,future==0.16.0,httpretty==0.8.14,nose==1.3.7,onepiece==0.1.0,pkg-resources==0.0.0
py3 runtests: PYTHONHASHSEED='1283052189'
py3 runtests: commands[0] | nosetests -c nose.cfg
......
Name                   Stmts   Miss  Cover
------------------------------------------
onepiece/__init__.py       1      0   100%
onepiece/example.py        2      0   100%
onepiece/main.py          22      0   100%
------------------------------------------
TOTAL                     25      0   100%
----------------------------------------------------------------------
Ran 6 tests in 0.056s

OK
海贼王(One Piece)
___________________________________ summary ____________________________________
ERROR:   py27: commands failed
  py3: commands succeeded

这里演示的是一个很简单的单元测试,Python 的单元测试可以详见:《 Python 中的单元测试

文档

独立一节来说,也是为了说明构建中文档是必须的环节,对于一个初始的项目,文档也很简单,大致包括:

  • License:开源项目,一般会有个开源许可证书声明,这里选择的是相对自由的 MIT License。关于各种开源证书的选择,可以参考文章:https://choosealicense.com/,或懒得想太多就参考:http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html
  • README:说明项目是干什么的、如何安装、如何使用、如何开发、如何发布,前三者是对于使用项目的人来说的,后两个一般是你的项目协作者需要关心的。如果内容很多,就不要都放在 README 了,可以拆分为多个文档;
  • Changelog:一开始也可以写在 README 中,当然后续维护的版本多了后,可以拆分为独立的文件维护;
  • Credits:可能有的人会写上贡献者的荣誉信息;
  • 有的还会加上一堆有用的第三方服务的 badges 来体现你的项目是一个靠谱的项目,比如:这里 onepiece 示例项目中的 CI/CD 构建是否通过、代码健康程度,以及测试覆盖率等等。

推荐用 Sphinx 写技术文档,尤其你写的是 Python 项目。当然,MarkDown 也很流行,很多人习惯用这个写文档。用 Sphinx 写技术文档,详见: 《 用 Sphinx 编写技术文档 》。

Ship it!

到了最重要的一个环节了,发布 Python 模块库到 PyPI,直接:

$ make sdist

想了解 make sdist 中具体命令,后续再写一篇如何发布到 PyPI,这里不展开说明。

为什么说发布很重要,原因很简单,你不发布,那前面那些对用户来说相当于没有发生。

现实是复杂的

  • 上述案例也就是个简化的项目,或者说是一个很小的类库级别的微型项目;
  • 项目规模大,单一的项目模板必然不适用,比如:多语言的项目;
  • 大项目必然会拆分成各个小项目,各个项目集成起来必然复杂;
  • 所谓的一键发布脚本以及 CI/CD 脚本,需要持续维护,可能逻辑随着项目复杂度上升,也会越来越复杂,一般一个团队都会让 0.5 个人维护这些东西,如果项目规模略大,那么就来个完整的 1 个人甚至是 1 个团队来维护也不夸张。

总结

我们用到的工具或服务有这些:

  • 配套工具:cookiecutter + tox + nose + coverage + Sphinx + EditorConfig + prospector
  • 配套服务:GitHub + Travis CI + Landscape + Coveralls + Read the Docs

配套工具和配套服务,前面提过,后续的文章中详细讲解模板里的文件时候再一并讲解,可以先简单看下这里: 《 做开源软件项目会用到的服务简介 》,对配套服务有简单介绍。

最后记住,大家一定要根据自己的需要形成自己的模板。

参考

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.io。并且部分内容已经公布在 GitHub 上:https://github.com/akun/pm