Python 项目的开发过程,其实就是一个或多个包的开发过程,而这个开发过程又由包的安装、管理、测试和发布等多个节点构成,所以这是一个复杂的过程,使用工具进行辅助开发有利于减少流程损耗,提升生产力。本章将介绍几个常用的、先进的工具,比如 setuptools、pip、paster、nose 和 Flask-PyPI-Proxy 等。
建议 70:从 PyPI 安装包
PyPI 全称 Python Package Index,直译过来就是“Python 包索引”,它是 Python 编程语言的软件仓库,类似 Perl 的 CPAN 或 Ruby 的 Gems。
$ pip install package_name
$ pip uninstall package_name
$ pip show package_name
$ pip freeze
建议 72:做 paster 创建包
distutils 标准库,至少提供了以下几方面的内容:
支持包的构建、安装、发布(打包)
支持 PyPI 的登记、上传
定义了扩展命令的协议,包括 distutils.cmd.Command 基类、distutils.commands 和 distutils.key_words 等入口点,为 setuptools 和 pip 等提供了基础设施。
要使用 distutils,按习惯需要编写一个 setup.py 文件,作为后续操作的入口点。在arithmetic.py同层目录下建立一个setup.py文件,内容如下:
from distutils.core importsetup
setup(name="arithmetic",
version='1.0',
py_modules=["your_script_name"],
)
setup.py 文件的意义是执行时调用 distutils.core.setup() 函数,而实参是通过命名参数指定的。name 参数指定的是包名;version 指定的是版本;而 py_modules 参数是一个序列类型,里面包含需要安装的 Python 文件。
编写好 setup.py 文件以后,就可以使用 python setup.py install 进行安装了。
distutils 还带有其他命令,可以通过 python setup.py --help-commands 进行查询。
实际上若要把包提交到 PyPI,还要遵循 PEP241,给出足够多的元数据才行,比如对包的简短描述、详细描述、作者、作者邮箱、主页和授权方式等:
setup(
name='requests',
version=requests.__version__,
description='Python HTTP for Humans.',
long_description=open('README.rst').read() + '\n\n' +
open('HISTORY.rst').read(),
author='Kenneth Reitz',
author_email='me@kennethreitz.com',
url='http://python-requests.org',
packages=packages,
package_data={'': ['LICENSE', 'NOTICE'], 'requests': ['*.pem']},
package_dir={'requests': 'requests'},
include_package_data=True,
install_requires=requires,
license=open('LICENSE').read(),
zip_safe=False,
classifiers=('Development Status :: 5 - Production/Stable','Intended Audience :: Developers','Natural Language :: English','License :: OSI Approved :: Apache Software License','Programming Language :: Python','Programming Language :: Python :: 2.6','Programming Language :: Python :: 2.7','Programming Language :: Python :: 3','Programming Language :: Python :: 3.3',
),
)
包含太多内容了,如果每一个项目都手写很困难,最好找一个工具可以自动创建项目的 setup.py 文件以及相关的配置、目录等。Python 中做这种事的工具有好几个,做得最好的是 pastescript。pastescript 是一个有着良好插件机制的命令行工具,安装以后就可以使用 paster 命令,创建适用于 setuptools 的包文件结构。
安装好 pastescript 以后可以看到它注册了一个命令行入口 paster:
$ paster create --list-template #查询目录安装的模板
$ paster create -o arithmethc-2 -t basic_package atithmetic #为了 atithmetic 生成项目包
简单地填写几个问题以后,paster 就在 arithmetic-2 目录生成了名为 arithmetic 的包项目。
用上 --config 参数,它是一个类似 ini 文件格式的配置文件,可以在里面填好各个模板变量的值(查询模板有哪些变量用 --list-variables参数),然后就可以使用了。
[pastescript]
description= corp-prj
license_name=keywords=Python
long_description= corp-prj
author=xxx corp
author_email=xxx@example.com
url= http://example.com
version= 0.0.1
以上配置文件使用paster create -t basic_package --config="corp-prj-setup.cfg" arithmetic
建议 73:理解单元测试概念
单元测试用来验证程序单元的正确性,一般由开发人员完成,是测试过程的第一个环节,以确保缩写的代码符合软件需求和遵循开发目标。好的单元测试有以下好处:
减少了潜在 bug,提高了代码的质量。
大大缩减软件修复的成本
为集成测试提供基本保障
有效的单元测试应该从以下几个方面考虑:
测试先行,遵循单元测试步骤:
创建测试计划(Test Plan)
编写测试用例,准备测试数据
编写测试脚本
编写被测代码,在代码完成之后执行测试脚本
修正代码缺陷,重新测试直到代码可接受为止
遵循单元测试基本原则:
一致性:避免currenttime = time.localtime()这种不确定执行结果的语句
原子性:执行结果只有 True 或 False 两种
单一职责:测试应该基于情景(scenario)和行为,而不是方法。如果一个方法对应着多种行为,应该有多个测试用例;而一个行为即使对应多个方法也只能有一个测试用例
隔离性:不能依赖于具体的环境设置,如数据库的访问、环境变量的设置、系统的时间等;也不能依赖于其他的测试用例以及测试执行的顺序,并且无条件逻辑依赖。单元测试的所有输入应该是确定的,方法的行为和结构应是可以预测的。
使用单元测试框架,在单元测试方面常见的测试框架有 PyUnit 等,它是 JUnit 的 Python 版本,在 Python2.1 之前需要单独安装,在 Python2.1 之后它成为了一个标准库,名为 unittest。它支持单元测试自动化,可以共享地进行测试环境的设置和清理,支持测试用例的聚集以及独立的测试报告框架。unittest 相关的概念主要有以下 4 个:
测试固件(test fixtures):测试相关的准备工作和清理工作,基于类 TestCase 创建测试固件的时候通常需要重新实现 setUp() 和 tearDown() 方法。当定义了这些方法的时候,测试运行器会在运行测试之前和之后分别调用这两个方法
测试用例(test case):最小的测试单元,通常基于 TestCase 构建
测试用例集(test suite):测试用例的集合,使用 TestSuite 类来实现,除了可以包含 TestCase 外,也可以包含 TestSuite
测试运行器(test runner):控制和驱动整个单元测试过程,一般使用 TestRunner 类作为测试用例的基本执行环境,常用的运行器为 TextTestRunner,它是 TestRunner 的子类,以文字方式运行测试并报告结果。
#测试以下类
classMyCal(object):defadd(self, a, b):return a +bdefsub(self, a, b):return a -b#测试
classMyCalTest(unittest.TestCase):defsetUp(self):print('running set up')deftearDown(self):print('running teardown')
self.mycal=NonedeftestAdd(self):
self.assertEqual(self.mycal.add(-1, 7), 6)deftestSub(self):
self.assertEqual(self.mycal.sub(10, 2), 8)
suite=unittest.TestSuite()
suite.addTest(MyCalTest("testAdd"))
suite.addTest(MyCalTest("testSub"))
runner=unittest.TextTestRunner()
runner.run(suite)
运行 python3 -m unittest -v MyCalTest 得到测试结果。
建议 74:为包编写单元测试
直接上一个实例:
__author__ = 'Windrivder'
importunittestfrom app importcreate_app, dbfrom flask importcurrent_appclassBasicsTestCase(unittest.TestCase):def setUp(self): #测试前运行
self.app = create_app('testing')
self.app_context=self.app.app_context()
self.app_context.push()
db.create_all()#创建全新的数据库
def tearDown(self): #测试后运行
db.session.remove()
db.drop_all()#删除数据库
self.app_context.pop()#测试程序实例是否存在
deftest_app_exists(self):
self.assertFalse(current_appisNone)#测试程序能在测试配置中运行
deftest_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
__author__ = 'Windrivder'
importtimeimportunittestfrom datetime importdatetimefrom app importcreate_app, dbfrom app.models importAnonymousUser, Follow, Permission, Role, UserclassUserModelTestCase(unittest.TestCase):deftest_password_setter(self):
u= User(password='Cat')
self.assertTrue(u.password_hashis notNone)deftest_no_password_getter(self):
u= User(password='Cat')
with self.assertRaises(AttributeError):
u.passworddeftest_password_verifycation(self):
u= User(password='Cat')
self.assertTrue(u.verify_password('Cat'))
self.assertFalse(u.verify_password('Dog'))deftest_password_salts_are_random(self):
u= User(password='Cat')
u2= User(password='Cat')
self.assertTrue(u.password_hash!=u2.password_hash)deftest_roles_and_permission(self):
Role.insert_roles()
u= User(email='john@example.com', password='cat')
self.assertTrue(u.can(Permission.WRITE_ARTICLES))
self.assertFalse(u.can(Permission.MODERATE_COMMENTS))deftest_anonymous_user(self):
u=AnonymousUser()
self.assertFalse(u.can(Permission.FOLLOW))deftest_timestamps(self):
u= User(password='cat')
db.session.add(u)
db.session.commit()
self.assertTrue(
(datetime.utcnow()- u.member_since).total_seconds() < 3)
self.assertTrue(
(datetime.utcnow()- u.last_seen).total_seconds() < 3)deftest_ping(self):
u= User(password='cat')
db.session.add(u)
db.session.commit()
time.sleep(2)
last_seen_before=u.last_seen
u.ping()
self.assertTrue(u.last_seen> last_seen_before)
以上代码是在学习 Flask 框架时,在书中学习到的单元测试。
建议 75:利用测试驱动开发提高代码的可测性
测试驱动开发(Test Driven Development,TDD)是敏捷开发中一个非常重要的理念,它提倡在真正开始编码之前测试先行,先编写测试代码,再在其基础上通过基本迭代完成编码,并不断完善。一般来说,遵循以下过程:
编写部分测试用例,并运行测试
如果测试通过,则回到测试用例编写的步骤,继续添加新的测试用例
如果测试失败,则修改代码直到通过测试
当所有测试用例编写完成并通过测试之后,再来考虑对代码进行重构
关于测试驱动开发和提高代码可测性方面有几点需要说明:
TDD 只是手段而不是目的,因此在实践中尽量只验证正确的事情,并且每次仅仅验证一件事。当遇到问题时不要局限于 TDD 本身所涉及的一些概念,而应该回头想想采用 TDD 原本的出发点和目的是什么
测试驱动开发本身就是一门学问
代码的不可测性可以从以下几个方面考量:实践 TDD 困难;外部依赖太多;需要写很多模拟代码才能完成测试;职责太多导致功能模糊;内部状态过多且没有办法去操作和维护这些状态;函数没有明显返回或者参数过多;低内聚高耦合等等。
建议 76:使用 Pylint 检查代码风格
如果团队遵循 PEP8 编码风格,Pylint 是个不错的选择(还有其他选择,比如 pychecker、pep8 等)。Pylint 始于 2003 年,是一个代码分析工具,用于检查 Python 代码中的错误,查找不符合代码编码规范以及潜在的问题。支持不同的 OS 平台,如 Windows、Linux、OSX 等,特性如下:
代码风格审查。它以 Guido van Rossum 的 PEP8 为标准,能够检查代码的行长度,不符合规范的变量名以及不恰当的模块导入等不符合编码规范的代码
代码错误检查。如未被实现的接口,方法缺少对应参数,访问模块中未定义的变量等
发现重复以及设计不合理的代码,帮助重构。
高度的可配置化和可定制化,通过 pylintrc 文件的修改可以定义自己适合的规范。
支持各种 IDE 和编辑器集成。如 Emacs、Eclipse、WingIDE、VIM、Spyder 等
能够基于 Python 代码生成 UML 图。Pylint0.15 中就集成了 Pyreverse,能够轻易生成 UML 图形
能够与 Hudson、Jenkins 等持续集成工具相结合支持自动代码审查。
使用 Pylint 分析代码,输出分为两部分:一部分为源代码分析结果,第二部分为统计报告。报告部分主要是一些统计信息,总体来说有以下6 类:
Statistics by type:检查的模块、函数、类等数量,以及它们中存在文档注释以及不良命名的比例
Raw metrics:代码、注释、文档、空行等占模块代码量的百分比统计
Duplication:重复代码的统计百分比
Messages by category:按照消息类别分类统计的信息以及和上一次运行结果的对比
Messages:具体的消息 ID 以及它们出现的次数
Global evaluation:根据公式计算出的分数统计:10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
我们来重点讨论一下源代码分析主要以消息的形式显示代码中存在的问题,消息以 MESSAGE_TYPE:LINE_NUM:[OBJECT:]MESSAGE 的形式输出,主要分为以下 5 类:
(C)惯例,违反了编码风格标准
(R)重构,写得非常糟糕的代码
(W)警告,某些 Python 特定的问题
(E)错误,很可能是代码中的 bug
(F)致命错误,阻止 Pylint 进一步运行的错误
比如如果信息输出 trailing-whitespace 信息,可以使用命令 pylint --help-msg="trailing-whitespace" 来查看,这里提示是行尾存在空格。
如果不希望对这类代码风格进行检查,可以使用命令行过滤掉这些类别的信息,比如 pylint -d C0303,W0312 BalancePoint.py。
Pylint 支持可配置化,如果在项目中希望使用统一的代码规范而不是默认的风格来进行代码检查,可以指定 --generate-rcfile 来生成配置文件。默认的 Pylintrc 可以在 Pylint 的目录 examples 中找到。如默认支持的变量名的正则表达式为:variable-rgx=[a-z_][a-z0-9_]{2,30}$,可以根据自己需要进行相应修改。其他配置如 reports 用于控制是否输出统计报告;max-module-lines 用于设置模块最大代码行数;max-line-length 用于设置代码行最大长度;max-args 用于设置函数的参数个数等。读者可自行查看 pylintrc 文件。
建议 77:进行高效的代码审查
建议 78:将包发布到 PyPI
可以是发布到官方的 PyPI 或者团队私有的 PyPI。这里先讲把包发布到官方的 PyPI,标准库 distutils 支持将包发布到 PyPI 的功能:
#现在 PyPI 上注册一个用户
$ python setup.py register#注册包名
$ python setup.py register -n#上传包
$ python setup.py sdist upload