开源Python项目结构

思考

当开始一个Python项目,我的意思是不仅仅是快速脚本的拼凑,而是作为一个产品化的程序,如何组织代码和文档,是需要精心考虑的。参考成熟的开源项目编程风范,使自己的程序能够不断迭代优化,是需要从开始就做好规划的。

工具和概念

以下是开发Python项目的工具和概念,可以帮助我们更好组织项目,编写更清晰的代码:

  • 项目层次(目录结构)

  • setuptoolssetup.py文件

  • git版本控制

  • 使用GitHub管理项目

    • GigHub的"Issues"用于以下功能:

      • bug跟踪

      • 功能需求

      • 未来计划

      • 发布/版本管理

  • git-flow实现git工作流

  • py.test实现单元测试

  • tox实现测试标准化

  • Sphinx用于自动生成HTML文档

  • TravisCI用于持续测试集成

  • ReadTheDocs用于持续文档集成

  • Cookiecutter用于将上述步骤自动化以启动下一个项目

项目层次(目录结构)

当设置一个项目,layout(或称为目录结构)是正确开始的重要步骤。一个恰当的项目层次意味着潜在的代码贡献者不必浪费时间找寻代码片段,凭直觉就能找到文件。

大多数项目都有一些顶层文件(类似setup.py, README.md, requirements.txt等),然后有3个目录是每个项目都具备的:

  • docs目录包含项目文档

  • 一个和项目名称相同的目录存储实际的Python包

  • 一个test目录位于2个位置:

    • 在包目录下的test目录包含测试代码和资源

    • 以及一个顶层的独立目录

以下是一个简单的sandman项目(注:这里采用了Open Sourcing a Python Project the Right Way文档中案例)

$ pwd
~/code/sandman
$ tree
.
|- LICENSE
|- README.md
|- TODO.md
|- docs
|   |-- conf.py
|   |-- generated
|   |-- index.rst
|   |-- installation.rst
|   |-- modules.rst
|   |-- quickstart.rst
|   |-- sandman.rst
|- requirements.txt
|- sandman
|   |-- __init__.py
|   |-- exception.py
|   |-- model.py
|   |-- sandman.py
|   |-- test
|       |-- models.py
|       |-- test_sandman.py
|- setup.py

上述,有一个docs目录(其中generated目录是一个空目录,sphinx将生成的文档存放在这个目录中),有一个sandman目录,以及在sandman目录下的test子目录。

setuptoolssetup.py文件

setup.py文件类似于其他通过diskutils包来使用其他包用于安装Python包。它是任何项目一个非常重要的文件,因为它包含了在PyPI中使用的版本信息,软件包环境需求,项目描述,以及你的名字和联系信息等等。这个文件使得软件包能够以一种程序化的方法被搜索和安装,提供了元数据和介绍工具。

setuptools包(实际上是diskutils的一个增强)将构建和分发Python软件包进行了简化。一个Python软件包可以通过setuptools打包并且和diskutils打包几乎完全一样。所以没有理由不使用setuptools

setup.py位于项目的根目录。最重要的setup.py部分称为setuptools.setup,也就是包含了包的所有元信息部分。

以下是完整sandman项目的setup.py

from __future__ import print_function
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
import io
import codecs
import os
import sys

import sandman

here = os.path.abspath(os.path.dirname(__file__))

def read(*filenames, **kwargs):
    encoding = kwargs.get('encoding', 'utf-8')
    sep = kwargs.get('sep', '\n')
    buf = []
    for filename in filenames:
        with io.open(filename, encoding=encoding) as f:
            buf.append(f.read())
    return sep.join(buf)

long_description = read('README.txt', 'CHANGES.txt')

class PyTest(TestCommand):
    def finalize_options(self):
        TestCommand.finalize_options(self)
        self.test_args = []
        self.test_suite = True

    def run_tests(self):
        import pytest
        errcode = pytest.main(self.test_args)
        sys.exit(errcode)

setup(
    name='sandman',
    version=sandman.__version__,
    url='http://github.com/jeffknupp/sandman/',
    license='Apache Software License',
    author='Jeff Knupp',
    tests_require=['pytest'],
    install_requires=['Flask>=0.10.1',
                    'Flask-SQLAlchemy>=1.0',
                    'SQLAlchemy==0.8.2',
                    ],
    cmdclass={'test': PyTest},
    author_email='jeff@jeffknupp.com',
    description='Automated REST APIs for existing database-driven systems',
    long_description=long_description,
    packages=['sandman'],
    include_package_data=True,
    platforms='any',
    test_suite='sandman.test.test_sandman',
    classifiers = [
        'Programming Language :: Python',
        'Development Status :: 4 - Beta',
        'Natural Language :: English',
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Operating System :: OS Independent',
        'Topic :: Software Development :: Libraries :: Python Modules',
        'Topic :: Software Development :: Libraries :: Application Frameworks',
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
        ],
    extras_require={
        'testing': ['pytest'],
    }
)

快速脚手架工具cookiecutter

cookiecutter 提供了一个快速创建模版的工具

pip install cookiecutter

然后执行

cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git

会提示一些问题,然后为你创建好目录结构及相应文件

README.md文件可以通过pandoc来自动生成README.rst是正确开始的重要步骤。一个恰当的项目层次意味着潜在的代码贡献者不必浪费时间找寻代码片段,凭直觉就能找到文件。

详细使用setuptools的方法参考Distributing Python Modules

python -m pip install setuptools wheel twine

源代码版本管理 - GitHub

使用git-flow来管理git工作流

建议使用 git-flow 分支模式。

  • develop是最主要的工作分之,也是用于部署下一个发行版的分支代码。

  • feature分支是相当于功能分之并且还没有部署的版本

  • 通过创建release分支来更新master主干

参考这篇来安装git-flow

在Mac平台有多种方法安装,我采用如下方法

curl -L -O https://raw.github.com/nvie/gitflow/develop/contrib/gitflow-installer.sh
sudo bash gitflow-installer.sh

然后执行以下命令将现有的项目迁移

git flow init

使用默认参数,完成后会自动创建developmaster分支。

大多数工作都是在develop分支,包含了所有完成的功能和bug fix用于发布;nightly builds或持续集成服务器将标记为develop

要开始一个新功能,使用

git flow feature start <feature name>

此时就会创建一个新的分支: feature/<feature name>。此时commit就会在这个分支,当功能就绪准备发布产品,就会合并到develop,则使用如下命令

git flow feature finish <feature name>

代码合并到develop分支并且会删除掉feature/<feature name>分支。

当准备发布产品时候,就从develop分支创建release分支。使用如下命令:

git flow release start <release number>

所有的完成并且准备发布功能的必须位于develop,这样才能feature finish。当创建了release分支,则可以发布代码。一些在release之后的小的bug fix则直接在release/<release number>分支。如果已经解决并且没有bug则使用如下命令:

git flow release finish <release number>

这就会合并releae/<release number>回到masterdevelop分支。

hotfix类似从master分离出的feature:如果你已经关闭了release分支但是意识到有必不可少的修改需要发布,则从master分离出hotfix分支(在git flow release finish <release number>之后)类似:

git flow hotfix start <release number>

然后在修改完成后,执行

git flow hotfix finish <release number>

virtualenvvirtualenvwrapper

如果在一个服务器上有多个Python项目,每个项目有不同的依赖,可以使用virtualenv来创建虚拟机的Python安装,这样site-packages,distributepip将按照这个方式独立。pip install将软件包安装到virtualeve而不是系统的Python安装。在不同的virtualenv之间切换是非常简单的一条命令。

virtualenvwrapper是一个单独的工具,用于创建和管理virtualenv:

pip install `virtualenvwrapper`

安装完成后,需要将路径添加到环境中

echo "source /usr/local/bin/virtualenvwrapper.sh" >> ~/.bashrc

创建虚拟化环境

mkvirtualenv ossproject

在虚拟环境中,很容易生成requirements.txt,因为pip可以通过requirements.txt-r参数安装任何项目依赖。要创建这个文件,执行以下命令

(ossproject)$ pip freeze > requirements.txt

测试py.test

Python标准库unittest包:nosepy.test。两者都扩展了unittest来使得容易添加附加功能。

测试设置:

test目录下,创建一个名为test_<project_name>.py文件,py.test的测试目录机制就会讲任何test_开头的文件作为一个测试文件(除非告诉它不怎么做)。

测试覆盖:

结合py.test,我们使用Ned Batchelder的coverage工具。要执行这个,线执行pip install pytest-cov

使用Tox完成标准化测试

Python的一个项目维护问题是兼容性。如果目标是支持Python 2.x和Python 3.x,则需要确保代码可以在指定平台正常工作。tox提供了"Pythong标准化测试":它能够创建一个完整的沙盒环境并安装你的软件包及其环境来进行测试。

tox是通过.ini文件:tox.ini来配置的,非常容易设置,以下是一个最小化的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中设置py26py27tox就可以知道需要针对哪种环境测试。并且tox默认就支持多种环境,例如jythonpypy

dpes是软件包的依赖,甚至可以告诉tox从一个替代的PyPI URL安装所有或部分依赖。最后,执行所有环境的检查

tox
  • setuptools集成

tox可以集成到setuptools这样python setup.py test就可以运行tox测试。以下代码片段是从tox文档中摘录加入到setup.py文件:

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},
    )

使用Sphinx撰写文档

Sphinx是从pocoo演化出来的工具,用于生成Python文档,从代码尽可能方便地自动生成HTML文档。

sphinx有一个类似javadoc的扩展,称为autodoc,可以从代码文档字符串中提取出reStructured文本。要能够充分使用Sphinx和autodoc,需要格式化代码文档字符串以便使用Sphinx的Python directives。

以下是一个模块功能使用了

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-apidoc -F -o docs <package name>

则会在docs目录下创建Sphinx的文档,并创建构建html输出的脚本。然后进入docs目录,创建输出

cd docs
make html

生成的文档输出到docs目录下的_build/html/子目录中

PEP 8

Jetbrains 默认支持PEP 8代码风格检查

缩进不符合规范

PEP 8: indentation is not a multiple of four

解决的方法有两种:

  • 重新格式化代码(推荐):选择Code菜单,然后选择Reformat Code就能够执行代码重新格式化,以符合标准。如果要修改通用的代码风格,例如每行缩进的空格数量,可以使用Code Style -> Python,可以调整缩进成词,空格,回行,空白行。在设置中可以搜索pep来关闭PEP8特定的设置。 - 参考 How to get rid of all the underlines that indicates a typo or too many spaces?

  • 忽略(不推荐):在任何提示信息的高亮代码行按下Alt-Enter,然后选择Ignore errors like this,这样就会在pycodestyle.py中加入W191到黑名单,忽略所有PEP8警告。 - 参考 get rid of PEP8 indentation warning in docstrings

函数名不符合规范

参考What is the naming convention in Python for variable and function names?

Python PEP8对于函数名和变量要求全部采用小写字母,单词间分隔采用下划线_;只有在遗留代码中已经存在混合大小写命名情况才使用mixedCase。

要忽略已经存在的这种变量或函数大小写,可以采用类似前述处理缩进忽略方法,按下Alt-Enter然后选择Ignore errors like this

另一种方法参考 function name should be lowercase,使用菜单File –>Settings–>Editor–>Inspections–>Python–>PEP 8 naming convention violation

参考

Last updated