社区化产品的长久生存之道可能莫过于对迭代周期的控制。还记得以前采用老土的阶段开发的年代,将软件生命周期分为各个阶段,当到达每个阶段的里程碑则集中所有的资源、人力作全面冲刺。每次到了里程碑的检查点冲过了就可以集体庆功,冲爬下了就集体加班。而后者发生的机率总是比前者要多,现在回想起来真有种大浪淘沙,不堪回首之感。

现在 敏捷开发 用顺溜了,回过头来看这种作坊式的开发甚是感触。阶段式的开发本身并无问题,而是迭代周期的控制很容易出错。往往都会将阶段周期拉得很长,尽量在每个阶段内将所有的工作完善之后再进入下一周期。然而,千里之堤,溃于蚁穴,过长的周期往往不会按我们预期的想法而进行,总是出现各种的问题,归结原因更多的是因为风险叠加的结果。优秀的PM会有N种处理风险的手段与经验,而且关于风险控制的理论层出不穷,这类的课程也是一扫一大堆。不过再强的PM再优秀的PM也架不住风险在里程碑的集中性爆发。

这可能是也是 敏捷开发 最吸引人的地方,因为风险的集中性爆发的影响被 持续集中 CI 给最小化了。本文的主题并不是全面地讨论敏捷的理论,我相信有敏捷开发实践的人并不在少数,真正驱动我写下本文的动力是自从.net 移居到 linux 这个大世界后发现持续集成是如此的简单,执行的成本是如此之低,各种敏捷的工具可谓一应俱全,很想将这个过程记录下来以供分享。

测试驱动 TDD

最近,在完成FreezesBeta版的开发,我就遇到发布问题,在微软平台上轻车熟路的做法现在得重新适应。Python 之所以诱惑人可能是她总是能给人惊喜吧。

多年强制实践敏捷的好处是可以彻底改掉不写测试的坏毛病,当测试写多了会自然萌生一种“不写测试就不安心”的感觉。 Python 世界有很优秀的的测试框架,例如:unittest, pyTest, nose, doctest 等等。由于 unittest 是内置框架,而且本人也比较懒所以很长时间内我也没用采用其它的测试框架,直至最近才发现 nose 在这诸多测试框架中的便利性,而且可以完全与 unittest 兼容还带有大量的代码断言工具,实在是很不错。关于 nose 的使用心得可以参考我发布在自己博客上的使用笔记:Nose 测试框架

我认为实践持续集成的核心就是TDD而不是小版本,因为通过测试就是验证小版本可发布的唯一标准。在体验 python TDD过程中不得不为 pyCharm 4 这个工具点赞!由其是当全面运行覆盖率检测时,pyCharm4已将UI与 coverage 很好的集成在一齐,可以很方便地查看项目中代码的测试覆盖情况:

另外在构建python测试有几个十分实用的工具

关于 TDD 的基础理论在此不作赘述有兴趣的朋友可以去度娘或者谷歌。

Python 发布包

当所有的测试通过后,一下步就是小版本的发布了。现在几乎没有什么开发语言体系是不具备官方的依赖包引用库的了,用 python 的话当然需要将可运行代码发布到 pypi 上,其它用户就能通过本地的命令行工具 pip 直接安装了(相当于做C#开发时直接从Nuget直接下载依赖包一样)。

python 的安装包是需要通过 setuptools 工具打包,生成 egg-info 和 发布包的。在代码中唯一需要做的工作就是编写 setup.py 文件。这个过程其实是瞒坑爹的,因为在python的包管理工具除了 setuptools 这个历史最为悠久的还有新一代的 distutils 工具,而官方说明非常地详细,具体可以参考Python Packaging User Guide 。 我花了老半天才将这份官方文档全部读完,但坑爹的是实作过程根本没有这么复杂,所以我在此总结了一下:

首先,在编写setup.py 之前需要一份依赖包引用文件 requirements.txt,(如果有就跳到下一步),在当前目录下进入命令行执行:

$ pip freeze

这里是 pip 的详细使用文档

执行成功后将会自动产生 requirements.txt。如果你不做这一步那么只能在 setup.py 内手工写 install_requires 了。

是在项目根目录下建立 setup.py文件,最简单的做法如下:

import os
import re
from setuptools import setup, find_packages # 读入依赖引用文件
with open('requirements.txt') as reqs_file:
REQS = reqs_file.read() setup(
name='项目名', # Pypi 显示的项目名
version='1.0',
packages=find_packages(exclude=['tests']),
install_requires=REQS, # 指定 setup 运行时的依赖包
)

这里有两个重点,一个是 find_packages,这个方法会在 setup.py 执行时将所有的 python 包(必须带有__init__.py)和包内的 .py 文件添加到打包目录中, 另一个就是 install_requires 这是 setup.py 在运行时自动检查环境内是否具备指定的依赖,如果没有就会自动下载安装。

写完 setup.py 就可以在命令行执行测试了

$ python setup.py build

注意,此处我并没有直接执行install 而只是使用 build 先将发布包生成至 build 目录并且输出 egg-info。通过这一步可以先检查最终发布包中是否有文件缺失。

如果 python 项目中包含有源码文件以外的资源需要打包发布,那么可以使用package_data属性,这个属性是一个“字典”类型,键(Key)值用于指定路径(当前项目路径是空串)。值(Value) 是一个文件数组,指定包含的文件资源的匹配表达式。如果是 Flask 的标准项目结构,要将 statictemplates 的内容包含于发布包内,那么:

#...

setup(
#...
package_data={
'': ['*.*', # 根目录下所有的文件类型
'static/**/*.*', # static 目录下所有的子目录及所有文件
'templates/*.*', # tempaltes 目录下所有的文件
'templates/**/*.*' # tempaltes 目录下所有子目录及所有文件
]
},
#...
)

以下是整个项目的完整 setup.py 文件

import os
import re
from setuptools import setup, find_packages ## 从源码目录中读取顶层包的 __version__ ,以便以后版本的统一更改
with open(os.path.join(os.path.dirname(__file__),
'这里是源码目录名', '__init__.py')) as init_py:
VERSION = re.search("VERSION = '([^']+)'", init_py.read()).group(1) # 读入依赖引用文件
with open('requirements.txt') as reqs_file:
REQS = reqs_file.read() setup(
name='Freezes',
version=VERSION,
packages=find_packages(exclude=['tests']),
install_requires=REQS, # 指定 setup 运行时的依赖包
package_data={
'': ['*.yml',
'*.json',
'*.cfg',
'layouts/*',
'seeds/*',
'static/**/*.*',
'templates/*.*',
'templates/**/*.*',
'translations/*.*'
]
},
entry_points={
'console_scripts': [
'freezes=freezes.server:main'
],
'setuptools.installation': [
'eggsecutable = freezes.server:main'
]
},
## 以下内容是可选的,用于生成 egg-info 的内容
url='http://freezes.dotnetage.com',
license='BSD',
author='Ray',
author_email='csharp2002@hotmail.com',
description="这里是项目简述,会在pipy的项目列表中显示",
long_description="这里是项目的详细描述,会在pypi项目详细页面中显示",
zip_safe=False,
platforms='any',
keywords=('static', 'flask'),
classifiers=['Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
'Topic :: Software Development :: Libraries',
'Topic :: Utilities']
)

到此,貌似所有的准备工作已准备完成,但恰恰这里可能就有个坑,我多次生成发布发现发布包缺少了文件,那么请加上 MANIFEST.in 并将项目根目录下的文件包含在内

** MANIFEST.in **

include requirements.txt

我在园子内找到一园友写的一篇博文就是关于 MANIFEST.in 的,详细参考 Python distribute到底使用package_data还是MANIFEST.in?

现在只要在pypi上注册一个帐户,然后回到项目的命令行状态运行:

$ python setup.py sdist upload

就可以生成安装包并直接上传到pypi上了,接下来就可以用 pip install <你的项目名> 检验你的发布成果了。

Github

在进行持续集成之前更重要的部署当然是源码控理了,关于 github 在此就不多说了,估计不会有人不知道它的大名的了。在发布到 Github 之前这里一份 .gitignore 文件可供参考,避免将无用的文件上传到Github:

.gitignore

.idea
.webassets-cache
*.pyc
*.pypirc # Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__ # Installer logs
pip-log.txt # Unit test / coverage reports
.coverage
.tox
nosetests.xml # SQLite databases
*.sqlite # Virtual environment
venv

如果你在使用pyCharm 那么推荐安装 .ignore 这个插件,可以直接支持多种的 ignore 文件。

自动构建

最后一公里就是就是自动构建,我们要达到的效果就是以后每次向 Github 提交变更时能自动执行部署和测试。如果条件允许可以使用Docker自建一台构建服务器完成这个过程。而另一个更佳做法是使用 Travis 的自动构建服务,只要用Github的账号注册,并将Reposiotry加入到Travis的跟踪项后,当Github上的项目发生变更时Travis就会自动从Github上将最新版本的源代码拉到一个独立的Docker环境内直接进行部署和测试,每次测试结束还会向你的邮箱发送测试报告。如果在项目的Readme文件内引入自动更新的状态标签PyPI Shields/Pins就将发布与最终用户之间的最后屏障打通。

Travis 可以支持很多的语言,文本以python为例,只要在项目的根目录内加入.travis.yml的配置文件,配置Travis的自动构建行为(这是可选的,如果没有此文件Travis 会执行默认构建)

以下是 .travis.yml 的完整内容:

language: python  #指定源码的语言
python: #指定python的版本
- "2.7"
- "pypy" # 执行安装前安装必要的依赖环境
before_install:
- sudo apt-get install node-less
- sudo apt-get install coffeescript # 执行安装指令
install:
- pip install -r tests/requirements.txt
- python setup.py -q install # 安装成功后执行的指令集,此处为自动执行测试
script: python tests/test_suites.py

最后就是将状态标签加入到的Readme文件内,让用户即时了解当前源码的状态,效果如下图:

要达到此效果只要在项目内加入readme.rst 文件并加以下以代码:

将以下变量替换为您的项目注册信息:

  • <github-username> - Github 用户名
  • <repository> - Github 源码项目名称
  • <pypi-project-name> - 在Pypi上发布的可执行包名
项目名称
======= .. image:: https://secure.travis-ci.org/<github-username>/<repository>.png?branch=master
:alt: Build Status
:target: http://travis-ci.org/<github-username>/<repository> .. image:: https://pypip.in/py_versions/<pypi-project-name>/badge.svg
:target: https://pypi.python.org/pypi/<pypi-project-name/
:alt: Supported Python versions .. image:: https://pypip.in/status/<pypi-project-name/badge.svg
:target: https://pypi.python.org/pypi/<pypi-project-name/
:alt: Development Status .. image:: https://pypip.in/version/<pypi-project-name/badge.svg
:target: https://pypi.python.org/pypi/<pypi-project-name/
:alt: Latest Version .. image:: https://pypip.in/license/<pypi-project-name/badge.svg
:target: https://pypi.python.org/pypi/<pypi-project-name/
:alt: License

小结

自此整个项目的持续环境搭建宣告完成,以后每个版本的迭代就只是管理 pypi 上的可运行版本与github上的源码版本即可。将这个方法延伸,则可应用于任何语言体验下的项目开发。同样只需要做的步骤:

  • 选定测试框架
  • 编写各种测试
  • 将运行版本发布至公共包管理库
  • 将源码发布至源码服务器 (github)
  • 接入构建服务器 (travis)

附:本文相关资源连接

TDD 与 CI 在 Python 中的实践的更多相关文章

  1. TDD在Unity3D游戏项目开发中的实践

    0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...

  2. 谈谈Python中元类Metaclass(二):ORM实践

    什么是ORM? ORM的英文全称是“Object Relational Mapping”,即对象-关系映射,从字面上直接理解,就是把“关系”给“对象”化. 对应到数据库,我们知道关系数据库(例如Mys ...

  3. Gitlab CI 持续集成的完整实践

    Gitlab CI 持续集成的完整实践 本着公司团队初创,又在空档期想搞点事情,搭建了私有Gitlab的契机,顺便把持续集成搭建起,实现了对Python服务端代码的单元测试.静态代码分析和接口测试的持 ...

  4. 如何在 Jenkins CI/CD 流水线中保护密钥?

    CI/CD 流水线是 DevOps 团队软件交付过程的基本组成部分.该流水线利用自动化和持续监控来实现软件的无缝交付.通过持续自动化,确保 CI/CD 流水线每一步的安全性非常重要.在流水线的各个阶段 ...

  5. Redis的Python实践,以及四中常用应用场景详解——学习董伟明老师的《Python Web开发实践》

    首先,简单介绍:Redis是一个基于内存的键值对存储系统,常用作数据库.缓存和消息代理. 支持:字符串,字典,列表,集合,有序集合,位图(bitmaps),地理位置,HyperLogLog等多种数据结 ...

  6. Python中的字符串与字符编码

    本节内容: 前言 相关概念 Python中的默认编码 Python2与Python3中对字符串的支持 字符编码转换 一.前言 Python中的字符编码是个老生常谈的话题,同行们都写过很多这方面的文章. ...

  7. python中的正则表达式(re模块)

    一.简介 正则表达式本身是一种小型的.高度专业化的编程语言,而在python中,通过内嵌集成re模块,程序媛们可以直接调用来实现正则匹配.正则表达式模式被编译成一系列的字节码,然后由用C编写的匹配引擎 ...

  8. paip.复制文件 文件操作 api的设计uapi java python php 最佳实践

    paip.复制文件 文件操作 api的设计uapi java python php 最佳实践 =====uapi   copy() =====java的无,要自己写... ====php   copy ...

  9. Python应用与实践【转】

    转自:http://www.cnblogs.com/skynet/archive/2013/05/06/3063245.html 目录 1.      Python是什么? 1.1.      Pyt ...

随机推荐

  1. CAC的Debian-8-64bit安装BBR正确打开方式

    装过三台debian 64 bit, CAC, 2欧, KVM虚拟机 做法都一样---下面说下正确安装方式   0. 有装锐速记得先删除,免得换核心后,锐速在扯后腿.   1.换4.9版kernel ...

  2. Distribution setup SQL Server Agent error: "RegCreateKeyEx() returned error 5, 'Access is denied.'" (转载)

    In the Configure Distribution Wizard, the step "Configuring SQL Server Agent to start automatic ...

  3. Jboss7.1 local EJB lookup problem

    We are trying to lookup for an Local EJB in JBoss7.1, but we get an ClassCast Exception. This local ...

  4. jboss-as- 7.1.1.Final配置jndi数据源

    初次使用jboss7.1.1.final部署项目,遇到了很多困难,最终通过查看官方文档和网上资料得以解决,特此记录一下. error information 2016-05-12 12:53:20 J ...

  5. LUA 运算笔记

    循环 比如要实现这样的一个For for(int i=10;i>1;i—) { print(i) } lua的for循环 转换成LUA for i=10,1,-1 do print(i) end ...

  6. html简单介绍(一)

    什么是html HTML 是用来描述网页的一种语言.HTML 指的是超文本标记语言 (Hyper Text Markup Language)HTML 不是一种编程语言,而是一种标记语言 (markup ...

  7. postgresql----几何类型和函数

    postgresql支持的几何类型如下表: 名字 存储空间 描述 表现形式 point 16字节 平面上的点 (x,y) line 32字节 直线 {A,B,C} lseg 32字节 线段 ((x1, ...

  8. 微信jsapi退款操作

    引自网络“ 前期准备:当然是搞定了微信支付,不然怎么退款,这次还是使用官方的demo.当然网上可能也有很多大神自己重写和封装了demo,或许更加好用简洁,但是我还是不提倡用,原因如下:(1)可能功能不 ...

  9. 拯救U盘之——轻松修复U盘“无法访问”的故障

    在使用U盘或者移动硬盘的过程中,大家是否和我一样,有个不好的操作习惯,明知不好但是在每次使用时都很少记得“安全删除硬件”,随手一把走人.终于出问题了,那天给mm复制完资料,拔了再插到自己的电脑上,打开 ...

  10. WorldWind源码剖析系列:网络下载类WebDownload

    网络下载类WebDownload封装了对请求的瓦片进行网络下载的相关操作.该类使用了两个委托类型和一个枚举类型. 该类的类图如下. 网络下载类WebDownload各个字段和属性的含义说明如下: st ...