设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

以正确的方式开源 Python 项目

2013-9-2 10:14| 发布者: 红黑魂| 查看: 9770| 评论: 0|来自: 开源中国编译

摘要: 大多数Python开发者至少都写过一个像工具、脚本、库或框架等对其他人也有用的工具。我写这篇文章的目的是让现有Python代码的开源过程尽可能清晰和无痛。我不是简单的指——“创建一个GitHub库,提交,在Reddit上发布 ...


测试安装

在测试目录里,无论你如何决定都要有这个目录,创建一个名为test_<project_name>.py的文件。py.test的测试发现机制将把所有test_前缀的文件当做测试文件处理(除非明确告知)。

在这个文件里放什么很大程度上取决于你。写测试是一个很大的话题,超出这篇文章的范围。最重要的,测试对你的和潜在的捐助者都是有用的。应该标识清楚每个用例是测试的什么函数。用例应该以相同的“风格”书写,这样潜在的贡献者不必猜测在你的项目中他/她应该使用三种测试风格中的哪种。

覆盖测试

自动化测试的覆盖率是一个有争议的话题。一些人认为它给出了错误的保证是一个毫无意义的度量,其他人认为它很有用。在我看在,我建议如果你已经使用自动化测试但从来没有检查过你的测试覆盖率,现在做这样一个练习。

使用py.test,我们可以使用Ned Batchelder的覆盖测试工具。使用pip安装pytest-cov。如果你之前这样运行你的测试:

$ py.test

你可以通过传递一些新的标识生成覆盖率报告,下面是运行sandman的一个例子:

$ py.test --cov=path/to/package 
$ py.test --cov=path/to/package --cov-report=term --cov-report=html
====================================================== test session starts =======================================================
platform darwin -- Python 2.7.5 -- pytest-2.3.5
plugins: cov
collected 23 items

sandman/test/test_sandman.py .......................
---------------------------------------- coverage: platform darwin, python 2.7.5-final-0 -----------------------------------------
Name                           Stmts   Miss  Cover
--------------------------------------------------
sandman/__init__                   5      0   100%
sandman/exception                 10      0   100%
sandman/model                     48      0   100%
sandman/sandman                  142      0   100%
sandman/test/__init__              0      0   100%
sandman/test/models               29      0   100%
sandman/test/test_sandman        114      0   100%
--------------------------------------------------
TOTAL                            348      0   100%
Coverage HTML written to dir htmlcov

=================================================== 23 passed in 1.14 seconds ===========================================================

当然不是所有项目都有100%的测试覆盖率(事实上,正如你读到的,sandman没有100%覆盖),但获得100%的覆盖率是一个有用的练习。它能够揭示我之前没有留意的缺陷与重构机会。

因为,作为测试本身,自动生成的测试覆盖报可以作为你持续集成的一部分。如果你选择这样做,部署一个标记来显示当前的测试覆盖率会为你的项目增加透明度(大多数时候会极大的鼓励他人贡献)。

使用Tox进行标准化测试

一个所有Python项目维护者都需要面对的问题是兼容性。如果你的目标是同时支持Python 2.x和Python 3.x(如果你目前只支持Python 2.x,应该这样做),实际中你如何确保你的项目支持你所说的所有版本呢?毕竟,当你运行测试时,你只使用特定的版本环境来运行测试,它很可能在Python2.7.5中运行良好但在Python 2.6和3.3出现问题。

幸运的是有一个工具致力于解决这个问题。tox提供了“Python的标准化测试”,它不仅仅是在多个版本环境中运行你的测试。它创造了一个完整的沙箱环境,在这个环境中你的包和需求被安装和测试。如果你做了更改在测试时没有异常,但意外地影响了安装,使用Tox你会发现这类问题。

通过一个.ini文件配置tox:tox.ini。它是一个很容易配置的文件,下面是从tox文档中摘出来的一个最小化配置的tox.ini:

# content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py26,py27
[testenv]
deps=pytest       # install pytest in the venvs
commands=py.test  # or 'nosetests' or ...

通过设置envlist为py26和py27,tox知道需要在这两种版本环境下运行测试。tox大约支持十几个“默认”的环境沙箱,包括jython和pypy。tox这个强大的工具使用不同的版本进行测试,在不支持多版本时可配置警示。

deps是你的包依赖列表。你甚至可以让tox从PyPI地址安装所有或一些你依赖包。显然,相当多的想法和工作已融入了项目。

实际在你的所有环境下运行测试现在只需要四个按键:

$ tox

 

一个更复杂的设置

 


我的书——“写地道的Python”,实际上写的是一系列的Python模块和代码。这样做是为了确保所有的示例代码按预期工作。作为我的构建过程的一部分,我运行tox来确保任何新的语法代码能正常运行。我偶尔也看看我的测试覆盖率,以确保没有语法在测试中被无意跳过。因此,我的tox.ini比上面的复杂一些,一起来看一看:

[tox]
envlist=py27, py34

[testenv]
deps=
    pytest
    coverage
    pytest-cov
setenv=
    PYTHONWARNINGS=all

[pytest]
adopts=--doctest-modules
python_files=*.py
python_functions=test_
norecursedirs=.tox .git

[testenv:py27]
commands=
    py.test --doctest-module

[testenv:py34]
commands=
    py.test --doctest-module

[testenv:py27verbose]
basepython=python
commands=
    py.test --doctest-module --cov=. --cov-report term

[testenv:py34verbose]
basepython=python3.4
commands=
    py.test --doctest-module --cov=. --cov-report term

这个配置文件依旧比较简单。而结果呢?

(idiom)~/c/g/idiom git:master >>> tox
GLOB sdist-make: /home/jeff/code/github_code/idiom/setup.py
py27 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip
py27 runtests: commands[0] | py.test --doctest-module
/home/jeff/code/github_code/idiom/.tox/py27/lib/python2.7/site-packages/_pytest/assertion/oldinterpret.py:3: DeprecationWarning: The compiler package is deprecated and removed in Python 3.x.
from compiler import parse, ast, pycodegen
=============================================================== test session starts ================================================================
platform linux2 -- Python 2.7.5 -- pytest-2.3.5
plugins: cov
collected 150 items 
...
============================================================ 150 passed in 0.44 seconds ============================================================
py33 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip
py33 runtests: commands[0] | py.test --doctest-module
=============================================================== test session starts ================================================================
platform linux -- Python 3.3.2 -- pytest-2.3.5
plugins: cov
collected 150 items 
...
============================================================ 150 passed in 0.62 seconds ============================================================
_____________________________________________________________________ summary ______________________________________________________________________
py27: commands succeeded
py33: commands succeeded
congratulations :)

(我从输出列表里截取了一部分)。如果想看我的测试对一个环境的覆盖率,只需运行:

$ tox -e py33verbose
-------------------------------------------------- coverage: platform linux, python 3.3.2-final-0 --------------------------------------------------
Name                                                                                           Stmts   Miss  Cover
------------------------------------------------------------------------------------------------------------------
control_structures_and_functions/a_if_statement/if_statement_multiple_lines                       11      0   100%
control_structures_and_functions/a_if_statement/if_statement_repeating_variable_name              10      0   100%
control_structures_and_functions/a_if_statement/make_use_of_pythons_truthiness                    20      3    85%
control_structures_and_functions/b_for_loop/enumerate                                             10      0   100%
control_structures_and_functions/b_for_loop/in_statement                                          10      0   100%
control_structures_and_functions/b_for_loop/use_else_to_determine_when_break_not_hit              31      0   100%
control_structures_and_functions/functions/2only/2only_use_print_as_function                       4      0   100%
control_structures_and_functions/functions/avoid_list_dict_as_default_value                       22      0   100%
control_structures_and_functions/functions/use_args_and_kwargs_to_accept_arbitrary_arguments      39     31    21%
control_structures_and_functions/zexceptions/aaa_dont_fear_exceptions                              0      0   100%
control_structures_and_functions/zexceptions/aab_eafp                                             22      2    91%
control_structures_and_functions/zexceptions/avoid_swallowing_exceptions                          17     12    29%
general_advice/dont_reinvent_the_wheel/pypi                                                        0      0   100%
general_advice/dont_reinvent_the_wheel/standard_library                                            0      0   100%
general_advice/modules_of_note/itertools                                                           0      0   100%
general_advice/modules_of_note/working_with_file_paths                                            39      1    97%
general_advice/testing/choose_a_testing_tool                                                       0      0   100%
general_advice/testing/separate_tests_from_code                                                    0      0   100%
general_advice/testing/unit_test_your_code                                                         1      0   100%
organizing_your_code/aa_formatting/constants                                                      16      0   100%
organizing_your_code/aa_formatting/formatting                                                      0      0   100%
organizing_your_code/aa_formatting/multiple_statements_single_line                                17      0   100%
organizing_your_code/documentation/follow_pep257                                                   6      2    67%
organizing_your_code/documentation/use_inline_documentation_sparingly                             13      1    92%
organizing_your_code/documentation/what_not_how                                                   24      0   100%
organizing_your_code/imports/arrange_imports_in_a_standard_order                                   4      0   100%
organizing_your_code/imports/avoid_relative_imports                                                4      0   100%
organizing_your_code/imports/do_not_import_from_asterisk                                           4      0   100%
organizing_your_code/modules_and_packages/use_modules_where_other_languages_use_object             0      0   100%
organizing_your_code/scripts/if_name                                                              22      0   100%
organizing_your_code/scripts/return_with_sys_exit                                                 32      2    94%
working_with_data/aa_variables/temporary_variables                                                12      0   100%
working_with_data/ab_strings/chain_string_functions                                               10      0   100%
working_with_data/ab_strings/string_join                                                          10      0   100%
working_with_data/ab_strings/use_format_function                                                  18      0   100%
working_with_data/b_lists/2only/2only_prefer_xrange_to_range                                      14     14     0%
working_with_data/b_lists/3only/3only_unpacking_rest                                              16      0   100%
working_with_data/b_lists/list_comprehensions                                                     13      0   100%
working_with_data/ca_dictionaries/dict_dispatch                                                   23      0   100%
working_with_data/ca_dictionaries/dict_get_default                                                10      1    90%
working_with_data/ca_dictionaries/dictionary_comprehensions                                       21      0   100%
working_with_data/cb_sets/make_use_of_mathematical_set_operations                                 25      0   100%
working_with_data/cb_sets/set_comprehensions                                                      12      0   100%
working_with_data/cb_sets/use_sets_to_remove_duplicates                                           34      6    82%
working_with_data/cc_tuples/named_tuples                                                          26      0   100%
working_with_data/cc_tuples/tuple_underscore                                                      15      0   100%
working_with_data/cc_tuples/tuples                                                                12      0   100%
working_with_data/classes/2only/2only_prepend_private_data_with_underscore                        43     43     0%
working_with_data/classes/2only/2only_use_str_for_human_readable_class_representation             18     18     0%
working_with_data/classes/3only/3only_prepend_private_data_with_underscore                        45      2    96%
working_with_data/classes/3only/3only_use_str_for_human_readable_class_representation             18      0   100%
working_with_data/context_managers/context_managers                                               16      7    56%
working_with_data/generators/use_generator_expression_for_iteration                               16      0   100%
working_with_data/generators/use_generators_to_lazily_load_sequences                              44      1    98%
------------------------------------------------------------------------------------------------------------------
TOTAL                                                                                            849    146    83%

============================================================ 150 passed in 1.73 seconds ============================================================
_____________________________________________________________________ summary ______________________________________________________________________
py33verbose: commands succeeded
congratulations :)

结果很可怕啊。

setuptools整合

tox可以和setuptools整合,这样python的setup.py测试可以运行你的tox测试。将下面的代码段放到你的setup.py文件里,这段代码是直接从tox的文档里拿来的:

from setuptools.command.test import test as TestCommand
import sys

class Tox(TestCommand):
    def finalize_options(self):
        TestCommand.finalize_options(self)
        self.test_args = []
        self.test_suite = True
    def run_tests(self):
        #import here, cause outside the eggs aren't loaded
        import tox
        errcode = tox.cmdline(self.test_args)
        sys.exit(errcode)

setup(
    #...,
    tests_require=['tox'],
    cmdclass = {'test': Tox},
    )

现在Python的setup.py测试将下载tox并运行它。真的很酷并且很节省时间。

Sphinx文档生成器

Sphinx是由pocoo团队开发的工具[@Lesus 注:pocoo团队开发了很多优秀的产品:如Flask, Jinja2等等]。它已经用来生成Python官方文档和大多数流行的Python包的文档。它以更容易的方式从Python代码中自动产生Python文档。

使用它完成工作

Sphinx不用了解Python程序以及怎样从它们中提取出来。它只能翻译reStructuredText文件,也就意味着你的代码文档的reStructuredText译文需要让Sphinx知道才能工作,但是管理维护所有的.py文件[至少是函数和类的部分]的reStructuredText译文显然是不可行的。

幸运的是,Sphinx有一个类似javadoc的扩展,叫做autodoc,可以用来从你的代码文档中萃取出reStructuredText。为了能够充分利用Sphinx和autodoc的能力,你需要已一种特别的方式格式化你的文档。特别是,你需要使用Sphinx的Python指令时。这里就是使用reStructuredText指令来为一个函数生成文档,使输出结果的HTML文档更漂亮:

def _validate(cls, method, resource=None):
"""Return ``True`` if the the given *cls* supports the HTTP *method* found
on the incoming HTTP request.

:param cls: class associated with the request's endpoint
:type cls: :class:`sandman.model.Model` instance
:param string method: HTTP method of incoming request
:param resource: *cls* instance associated with the request
:type resource: :class:`sandman.model.Model` or None
:rtype: bool

"""
if not method in cls.__methods__:
    return False

class_validator_name = 'validate_' + method

if hasattr(cls, class_validator_name):
    class_validator = getattr(cls, class_validator_name)
    return class_validator(resource)

return True

文档需要花费一点功夫,但是为了你的使用者,这个付出是值得的。好吧,好的文档使一个可用的项目去其糟粕。

Sphinx的autodoc扩展让我们可以使用很多指令,而这些指令可以自动的从你文档中生成文档。

安装

确认将Sphinx安装在你的virtualenv内,因为文档在项目里也是按版本来的。Sphinx不同的版本可能会产生不同的HTML输出。通过将其安装在你的virtualenv内,你可以以受控的方式升级你的文档。

我们要保持我们的文档在docs文件夹,将文档生成到docs/generated文件夹。在项目的根目录运行以下命令将根据你的文档字符自动重构文本文档:

$ sphinx-apidoc -F -o docs <package name>

这将产生一个包含多个文档文件的docs文件夹。此外,它创建了一个叫conf.py的文件,它将负责你的文档配置。你还会发现一个Makefile,方便使用一个命令(生成html)构建HTML文档。

在你最终生成文档之前,确保你已经在本地安装了相应的包(尽管可以使用pip,但python setup.py develop是最简单的保持更新的方法),否则sphinx-apidoc无法找到你的包。

配置:conf.py

conf.py文件创建用来控制产生的文档的各个方面。它自己会很好生成文档,所以我只简单地触及两点。

版本和发布

首先,确保你的版本和发布版本号保持最新。这些数字会作为生成的文档的一部分显示,所以你不希望它们远离了实际值。

保持你的版本最新的最简单方式就是在你的文档和setup.py文件中都从你的包的__version__属性读取。我从Flask的conf.py借用过来配置sandman的conf.py:

import pkg_resources
try:
    release = pkg_resources.get_distribution('sandman').version
except pkg_resources.DistributionNotFound:
    print 'To build the documentation, The distribution information of sandman'
    print 'Has to be available.  Either install the package into your'
    print 'development environment or run "setup.py develop" to setup the'
    print 'metadata.  A virtualenv is recommended!'
    sys.exit(1)
del pkg_resources

version = '.'.join(release.split('.')[:2])

这就是说,为了让文档产生正确的版本号,你只需在你的项目的虚拟环境中简单的需要运行$python setup.py develop即可。现在你只需担心保持__version__为最新,因为setup.py会使用它。

html_theme 
考虑到更改default到html_theme,我更喜欢原生态的东西,显然这是一个个人喜好的问题。我之所以提出这个问题是因为Python官方文档在Python 2和Python 3将默认主题更改为Pydoc主题(后者的主题是一个自定义主题仅在CPython源代码中可用)。对一些人来说,默认的主题使一个项目看起来“老”一些。

PyPI

PyPI,Python包索引(以前被称为“Cheeseshop”)是一个公开可用的Python包中央数据库。PyPI是你的项目发布的地方。一旦你的包(及其相关的元数据)上传到PyPI,别人通过pip或easy_instal可以下载并安装它。这一点得强调一下:即使你的项目托管在GitHub,直到被上传到PyPI后你的项目才是有用的。当然,有些人可以复制你的git库任何直接手工安装它,但更多的人想使用pip来安装它。

最后的一步

如果你已经完成了所有的前面部分中的步骤,你可能急着想把你的包上传到PyPI,供其他人使用!

先别急着做上述事情,在分发你的包之前,有一个叫做cheesecake的有用的工具有助于运行最后一步。它分析你的包并指定一个分类的数字分数。它衡量你的包在打包、安装、代码质量以及文档的数量和质量方面是否容易/正确。

除了作粗略衡量的“准备”,cheesecake在完整性检查方面很优秀。你会很快看到你的setup.py文件是否有错或者有没有忘记为一个文件制作文档。我建议在上传每个项目到PyPI之前运行一下它,而不仅只是第一个。

初始化上传

现在,你已经确定了你的代码不是垃圾和当人们安装它时不会崩溃,让我们把你的包放到PyPI上吧!你将会通过setuptools和setup.py脚本交互。如果这是第一次上传到PyPI,你将首先注册它:

$ python setup.py register
注意:如果你还没有一个免费的PyPI账户,你将需要现在去注册一个,才能注册这个包[@Lesus 注:注册之后还需要到邮箱去验证才行]。在你已使用了上面注册之后,你就可以创建发布包和上传到PyPI了: 
$ python setup.py sdist upload

上面这个命令建立一个源码发布版(sdist),然后上传到PyPI.如果你的包不是纯粹的Python(也就是说,你有二进制需要编译进去),你就需要发布一个二进制版,请看setuptools文档,了解更多。

发布及版本号

PyPI使用发行版本模型来确定你软件包的哪个版本是默认可用的。初次上传后,为使你软件包的每次更新后在PyPI可用,你需要指定一个新版本号创建一个发布。版本号管理是一个相当复杂的课题,PEP有专门的内容:PEP 440——版本识别和依赖指定。我建议参照PEP 400指南(明显地),但如果你选择使用不同版本的方案,在setup.py中使用的版本比目前PyPI中的版本“高”,这样PyPI才会认为这是一个新版本。



酷毙
1

雷人
1

鲜花

鸡蛋

漂亮

刚表态过的朋友 (2 人)

  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号