Django-独立应用教程-二-

118 阅读49分钟

Django 独立应用教程(二)

原文:Django Standalone Apps

协议:CC BY-NC-SA 4.0

十四、管理版本兼容性

当您编写一个 Django 应用来包含在您自己的项目中时,您已经知道 Python、Django 和其他所有使用的依赖项的版本。当你创建一个独立的应用时,你既不知道也不能控制这些版本,因为它们会被部署到其他人的项目中。因此,对其他开发人员来说,“对我有用”的东西可能甚至在微妙不同的环境中也不起作用。您可能无法确切知道每个用户和潜在用户部署了哪些版本,但您可以预测 Python、Django 甚至依赖版本的主要组合,并确保您的应用在每个版本中都可以工作。这有一个额外的好处,无论你在哪里使用你自己的独立应用,升级都变得更加容易。

这里的关键工具是测试这些版本差异的策略,以及同时支持不同的可能不兼容的 Python、Django 版本和其他库依赖的策略。

Python 版本

Python 版本的差异可能看起来是最难解决的版本差异,但随着 Python 2 官方支持的结束,大多数独立应用需要解决的实际差异不再那么重要。也就是说,在某些情况下,您会发现某些功能在一个 Python 版本中有效,但在另一个版本中无效。例如,Python 的 f 字符串是在 Python 3.6 中添加的,如果你的目标是完全支持 Django 2.2 作为长期支持版本,那么你需要支持 Python 3.5。因此,f 字符串应该用标准的字符串格式替换。类似地,赋值表达式,俗称 walrus 运算符,仅在 Python 3.8 中添加,因此在您的独立应用中使用它们会阻止任何运行 Python 3.7 的人使用您的应用。

这就涉及到您需要解决的关于 Python 版本的主要问题,即支持哪些版本。如果支持额外版本的成本很低,那么宁可支持这些版本。这可能意味着 Python 或其他解释器的另一个版本号。大多数部署肯定运行在 CPython 上,但这不是运行 Python 的唯一方式。在撰写本文时,唯一支持 Python 3 的主要替代实现是 PyPy,一个 JIT 编译器;Jython 和 IronPython,Java 和。NET 运行时实现最高只支持 Python 2.7。

针对不同版本的 Python 进行测试的工作方式与预期的非常相似——设置版本独特的虚拟环境,并在每个环境中运行测试:

$ python3.6 -m venv venvs/python36
$ source venvs/python36/bin/activate
$ python setup.py install
$ ./runtests.py
$ python3.7 -m venv venvs/python37
$ source venvs/python3.7/bin/activate
$ python setup.py install
$ ./runtests.py
$ python3.8 -m venv venvs/python38
$ source venvs/python3.8/bin/activate
$ python setup.py install
$ ./runtests.py

然而,这将很快变得乏味且容易出错。相反,我们可以用测试工具 tox 替换整个结构和流程,就像这样:

$ pip install tox
$ tox

首先,我们需要一个最小的 tox.ini 配置文件,以便 tox 知道要创建什么环境以及要在其中安装什么:

[tox]
envlist = py36, py37, py38

[testenv]
setenv =
    PYTHONPATH = {toxinidir}:{toxinidir}/myapp
commands = python runtests.py
basepython =
    py36: python3.6
    py37: python3.7
    py38: python3.8
deps =
    -r{toxinidir}/requirements.txt

该文件有两个组成块,tox 和 testenv。第一个是我们声明默认环境的地方。如果它们不存在,将会创建它们,并且每次在没有指定环境的情况下运行 tox 时,测试都会在其中运行。

testenv 的第二个模块是我们指定什么进入测试环境,如何运行测试,以及我们指定 Python 版本的地方。basepython 中描述的每一项都应该对应一个可执行名称。这也是您可以包含替代 Python 实现的地方:

basepython =
        pypy: pypy
    py36: python3.6
    py37: python3.7
    py38: python3.8

这里要指出的另一项是 deps 配置。这允许您指定在哪些环境中安装哪些依赖项。对于这个基本示例,我们将假设所有应用和测试依赖项都在 requirements.txt 文件中定义,当工具运行时,每个依赖项都将从该文件安装在各自的 tox 环境中。

Django 和附属地

您需要关注的最明显也是最重要的版本差异是不同的 Django 版本。主要的版本变化带来了不赞成和突破性的改变,如果你的独立应用运行的 Django 版本不同于你最初测试的版本,可能会导致意想不到的错误。

  • 关于版本锁定的一句话:虽然将 Django 作为独立应用的一个要求是一个好主意,但要小心不要过于激进地设置版本界限。只有当你的独立应用的当前版本与已发布或即将发布的版本之间存在已知的不兼容时,才应该设置上限。较低的边界同样应该代表来自已知和不支持的版本问题的安全性。如果你决定不支持较低版本的 Django,设置一个最低版本要求将有助于确保开发者用户在你的应用中只使用已知的工作环境。这也意味着,即使它恰好适用于其他人需要使用的 Django 版本,他们也无法使用。

您将面临的主要问题是测试和支持哪个版本。如果您自己的项目中没有任何需要新版本特性的特殊需求,一个好的经验法则是以 Django 开源项目本身支持的 Django 版本为目标。这意味着最新版本和当前的长期支持版本。在 2020 年初,这将意味着 Django 3.0 和 2.2 (LTS)。

如果您的独立应用依赖于其他 Django 应用,您可能会面临类似的问题。如果这些应用不提供类似的版本覆盖,事情可能会变得更加复杂。

在图 14-1 中,我们比较了三种不同的依赖项(命名为 A、B 和 C)及其各自的 Django 版本支持范围。如果这些依赖项都是必需的,那么您自己的独立应用所支持的版本会受到它们所支持的 Django 版本的限制,用虚线表示。

img/486718_1_En_14_Fig1_HTML.jpg

图 14-1

兼容 Django 版本的范围

你也可能遇到这样的问题,例如,依赖项 C 只支持 Django 3.0 和更高版本,而依赖项 A 只支持 Django 2.2,但作为可选的依赖项,而不是必需的依赖项。这种情况不太可能发生,但在支持应用中的可选功能时会发生。

解决不兼容问题

API 中的更改需要有条件的特性命名和导入。可能这意味着尝试在你的独立应用的多个模块中多次导入正确的名称,无论这些名称是否相同。这样做的问题不是它不能工作,而是它会使你的模块变得混乱,并且需要重复代码。

解决方案是将所有特性和版本条件的导入和定义合并到一个模块中,就像应用设置一样。

try:
        from django.urls import reverse
except ImportError:
        from django.core.urlresolvers import reverse

try:
        from third_party.lib import cool_function
except ImportError:
        from third_party.utils import cool_function

一个常见的约定是简单地将它们包含在 compat.py 模块中。从历史上看,这对于支持 Python 2 和 Python 3 都是至关重要的,但是您可能会发现对于 Django 版本、第三方依赖性,甚至 Python 版本的差异,这也是必要的。

如果有必要,不要害怕供应商。这可能是复制一个单独的函数,甚至是一个模块,如果它对你的独立应用很重要,但在你想要支持的 Django 或其他依赖版本中不可用的话。当您这样做时,请记住包括并遵守所有许可条款。

面向未来

即使您决定在发布应用后不希望对其进行任何功能更新,您也可能会发现它使用的 Django 功能变得越来越不值钱。确保您的应用继续与新版本的 Django(和 Python)一起工作的基础是不断测试最新版本的 Django 和 Python,甚至是未发布或不受支持的版本。

下面的基本 tox 文件是为测试 Django 的两个不同的 LTS 版本和(假设的)预发布版本 4.0a1 而设计的。预发布包可以发布到 PyPI 并使用其固定版本下载,但不能使用 ranges 安装。缺点是,随着后续预发布版本的发布,您可能需要对此进行更新。

[tox]
envlist = py37, py38

[testenv]
setenv =
    PYTHONPATH = {toxinidir}:{toxinidir}
commands = python runtests.py
basepython =
    py37: python3.7
    py38: python3.8
deps =
        django22: Django>=2.2,<3
    django32: Django>=3.2,<4
        django40: Django==4.0a1
    -r{toxinidir}/requirements-test.txt

摘要

在本章中,您了解了支持不同版本的 Python 和 Django 所带来的挑战,使用附加依赖项时的依赖范围问题,以及解决这些问题的策略。这些解决方案包括依赖专用的兼容性模块,以及严格测试 Python 和 Django 版本的组合。

在下一章中,我们将研究为不仅仅支持 Django 的应用提供多个框架和后端目标。

十五、混合依赖支持

在前一章中,你学习了如何管理你的应用与不同版本的 Python 和 Django 之间的版本兼容性。在这一章中,我们将超越 Django,着眼于提供与 Django 和其他非 Django 相关库的特性兼容性。

超越 Django

Django 应用中的功能,即使是“独立的”应用,也不需要包含在仅包含 Django 的包中。您可能会发现,您希望在一个独立的 Django 应用中提取或包含的核心功能很大程度上不是特定于 Django 的,而且,您希望在 Django 项目之外也能使用这些核心功能。这留给你几个选择。一种是创建一个独特的基础包,它是 Django 或一般框架不可知的,然后是一个单独的特定于 Django 的包。这是一个非常有效的策略。第二个策略是创建一个包含特定于 Django 的功能的包,或者甚至是其他框架的功能,作为独立的contrib模块与您的包一起提供。

对于特定于框架的功能,即捆绑的 Django 独立应用,主要是核心和框架无关功能的框架适配器的情况,第二种策略应该简化开发和包的维护。运送一个可能不被使用的模块的“负面影响”应该被认为是最小的,特别是与维护单独的包和增加其他开发人员用户的依赖性需求的成本相比。

螺母和螺栓

考虑一个先进的 lorem ipsum 生成器。 Lorem ipsum 是设计师经常使用的伪拉丁文本,用于填充设计的内容区域,包括网站,以便其他利益相关方在最终内容不可用时可以对设计有所了解,例如:

  • Lorem ipsum 疼痛静坐 amet,结果导致精英肥胖,渴望 eiusmod 的时间煽动起来的劳动和巨大的阿喀琉斯痛苦。

Django 甚至附带了一个内置的模板标签 lorem,它将生成以下文本:

{% lorem 5 p %}

但是你已经决定超越这一点,允许你的团队或任何人能够从不同的和特定标签的语料库中生成类似于 lorem 的占位符文本,包括技术流行语、MBA 行话和潮人-lorem。

该解决方案显然是用一个新的模板标记实现的,您称之为 lorem_plus,并且具有与内置 lorem 标记类似的接口:

{% lorem_plus 'hipster' 1 %}

这将从指定的语料库中返回一些占位符文本:

  • 动物标本冥想 humblebrag,stumptown migas 斜挎包慢碳水化合物。

虽然在 Django 项目中使用它所需的实现是特定于 Django 的——Django 模板标签在其他地方或多或少是无用的——但核心功能是相当通用的。这包括选择一个语料库,组装一些“句子”,将它们打包成一种或另一种格式的段落,然后可选地包装输出(例如,作为安全标记)。无论是在 Django 项目还是 Flask 项目中,它对 Jinja 模板都非常有用。

这不仅可以通过从特定于 Jinja 的代码中分割出特定于 Django 模板的代码来实现,还可以通过分割核心功能本身来实现。代替像这样的结构

templatetags/
        __init__.py
        lorem_tags.py
__init__.py

包模块可能具有这样的结构:

templatetags/
        __init__.py
        lorem_tags.py
__init__.py
core.py
jinja_tags.py

core.py 模块将拥有所有的“业务逻辑”,包括 lorem 生成函数 lorem_generator,它返回每个模板实现可以标记为安全呈现的基本字符串。这里可能是我们的函数签名(这里省略了主体,因为我们的目的不需要):

# core.py
def lorem_generator(corpus, count=1, method="b"):
        """
        Returns randomized placeholder text

        Args:
                corpus: string identifying the corpus
                count: number of words or paragraphs
                method: words 'w', HTML paragraphs 'p',
                        or plaintext paragraphs 'b'

        Returns: a string
        """

那么模板后端实现所需要的就是调用这个函数并返回标记为安全呈现的字符串,对于 Django:

# lorem_tags.py
@register.tag
def lorem_plus(corpus, count=1, method="b"):
        placeholder = lorem_generator(corpus, count, method)
        return mark_safe(placeholder)

对金佳来说:

# jinja_tags.py
def lorem_plus(corpus, count=1, method="b"):
        placeholder = lorem_generator(corpus, count, method)
        return jinja2.Markup(placeholder)

现在,相同的功能不仅可以在模板后端使用,还可以在框架中使用,因为应用中的 Django 功能只是核心功能的实现细节。

真实世界的例子

这种特殊的场景并不常见,尽管它非常有用。

WhiteNoise 是一个静态文件服务实用程序,旨在简化生产网站中的静态文件服务。它是一个 Python 包,支持与 Django 用来与生产应用服务器接口的 WSGI (Web 服务器网关接口)协议相同的协议。因此,它可以用于任何 WSGI 应用,Django 或其他。然而,对于 Django 有特定的启示,允许在 Django 项目中集成 WhiteNoise,而不是在 WSGI 级别,这有利于开发中的方便集成、预发布任务的集合静态管理命令和中间件。

所有这些功能都可以通过将此功能包含在任何核心功能都不需要(即不导入)的模块中来支持。为了简化在开发中使用 WhiteNoise 否则可以通过向 runserver 管理命令传递- nostatic 来启用——可以将包含的 Django 应用添加到项目的 INSTALLED_APPS 列表中。

INSTALLED_APPS = [
    'whitenoise.runserver_nostatic',
    'django.contrib.staticfiles',
    # everything else
]

从功能上来说,runserver_nostatic 应用只不过是一个扩展 runserver 命令的管理命令。然而,与包含的中间件相结合,它使 WhiteNoise 的所有功能能够在 Django 项目中无缝地使用,并且不会损害使用 Flask 的人的核心功能的有用性。

这是一个真实世界的例子,它将一些小的修改或集成从通用功能集成到 Django 项目中。现在应该不难看出,这也可以通过更深度集成的功能来实现。

nplusone 是一个用于“检测 Python ORMs 中的 n+1 查询问题”的实用程序。这是基于 ORM 的应用中最常见的与数据库相关的性能问题之一,在这种情况下,从数据库中返回某个模型的列表(queryset)会导致不是一个查询,而是一个针对返回的每一项加上原始查询的查询。这是从相关模型中获取属性的结果,在 Django 应用中,最常见的解决方法是使用 select_related 或 prefetch_related。然而,这不是 Django 特有的问题,nplusone 在一个包中支持主要的 Python ORMs,包括 Django、SQLAlchemy 和 Peewee。

这里的主要问题不是简单地提供一些核心功能的小改动。相反,每个受支持的 ORM 都需要自己的一套独特的特性。基础或核心模块提供了一些常见的“脚手架”式的异常和信号管理,但是 ORM 特定的实现是独特的。

自然的问题是,为什么不把这些作为单独的包装运输呢?不代表维护者,它确实提供了更简单的开发和维护,更不用说项目营销了。可能更重要的是,或者更具体地说,它允许跨实现捕获特定于领域的变化。一个增加了对查询中未使用的数据属性的检查的版本,是由一个并不特定于任何一个 ORM 的问题驱动的,它有助于在一个新版本中跨每个实现发布,而不是针对同一领域特性的一系列单独的发布。

摘要

在这一章中,你学习了如何将特定于 Django 和特定于后端的特性与更一般的特性分开,以允许在 Django 项目之外和/或使用不同的支持类(例如,模板后端)重用应用功能。您了解了可以将功能分离到不同的已发布包中,或者简单地利用已发布包中的替代模块来简化开发,同时保持库的可扩展性。在下一章中,你将了解水平和垂直模块化的含义,以及如何使用这两种细分范例来帮助组织你的应用。

十六、模块性

我们将 Django 项目分解成应用,按照横向编程功能和纵向业务特性进行细分,使它们更容易使用和推理,当然,这些组件也更容易重用。

其中一些细分市场的定义比其他细分市场更严格,导致应用更小和/或更窄。例如,比较 django-model-utils 和 django-extensions。两者都以有用的模型和字段类的形式提供了一些重叠的特性,但是 django-model-utils 主要关注于解决重复的与模型相关的功能,而 django-extensions 主要关注于解决在 django 项目中有用的更一般的特性,并且恰好包括了这种与模型相关的功能。并不是说一个比另一个好;更确切地说,每一个的范围都源于它所解决的问题领域。

这就是说,一些问题领域有助于扩大范围,即使问题可以被简明地定义。“管理用户在网站上创建的内容”是一个很好定义的业务问题,但是在实践中包含了各种不平凡的子需求。此外,这些子需求——如管理多媒体或特定于用户的内容——在许多用例中可能并不需要。

这将决定是否以及如何进一步模块化你的独立应用,包括使用子应用和额外的独立应用。

附加独立应用

将一个更大的独立应用分成更多的独立应用将是恰当的。这是一种进一步细分应用的方式,例如,通过垂直业务功能,以便子组件紧密集中。它有它的用途,但也有一些成本,特别是作为一个主要的策略。

采用这种策略并将一个较大的独立应用分解为独立应用组件的好处与首先创建一个独立应用的好处是一致的。分离的应用可以用较小的代码库来开发、测试和重用,允许用户只安装他们的项目所需的组件。

然而,这种策略有一些明显和不太明显的缺点。

首先,对于维护者来说,维护独立的包会降低边际价值,增加边际成本。核心应用中的向后不兼容或突破性变更意味着必须在组件应用和并行版本之间协调并行变更。当所有的更改都可以在一个包中编排时,这项工作就容易多了,可以更好地利用重构工具和测试的公共测试。

其次,它回避了一个问题,即核心应用——我们在这里假设的——本身是否足够有用。拥有一个比常用的基础包稍微多一点的核心应用肯定会有价值,但如果是这样,那么最有可能的是,与其说它是一个独立的应用,不如说它是一个有用的基础包,可以与独立的独立应用一起使用。

第三,这对你的开发者用户来说是额外的麻烦。使用更细粒度的依赖关系有很多好处,比如不包含不需要的代码,这可能会导致不必要的部署膨胀,或者暴露于无关的错误和兼容性问题。它还添加了更多要跟踪的独立依赖项。

应该采取这种策略的时候是,当次要功能预计将被选择加入,并且具有插件的性质时,当功能可能在没有核心应用的情况下具有重要的用例,因此它本身作为安装包是有用的,或者当它的管理与核心应用更好地分离时。如果子组件受益于更快的发布周期,情况可能就是这样。包耦合本来会使子组件与核心保持同步变得更容易,但现在它可能会阻碍子组件的有价值的发布。

这种分离的一个例子是 django-localflavor,它以前的名字是 django.contrib.localflavor。作为一个特定于国家的实用程序的存储库,比如州和省的列表,以及验证邮政编码和电话号码的表单和模型字段,它的功能不仅仅是一个功能库,也是一个知识库。分离出这个子组件允许将焦点从框架的编程工具和特定于地区的知识积累中分离出来。

使用子应用

创建单独的独立应用的一个可行且常用的替代方法是将您的独立应用分解成子应用,这些子应用都包含在主包中。这是几乎所有基于 Django 的 CMS 都采用的策略,包括 Wagtail、Django CMS 和 Mezzanine。当然,Django 本身在一个整合的包 django.contrib 中提供了多个相关的应用。

django.contrib 示例既是这种工作方式的一个例外,也很有说明性。这是一个例外,因为它当然附带了框架,但也没有真正的“核心”应用,例如,你不能将 django.contrib 添加到 INSTALLED_APPS 中。django 有一个依赖网络。contrib.auth、contrib.admin 和 contrib.sites 都需要 contenttypes,但是每个都解决了一个通常不相关的业务需求。

尽管针对不同的业务需求,这些应用经常一起使用,因此有着共同的包装。它们不需要全部安装在您项目的 INSTALLED_APPS 应用中,并且未使用的应用的存在对于开发人员用户来说没有什么坏处。

当您的子组件是单独的可安装应用时,它们需要单独安装才能用作应用(例如,使用模型、模板、模板标签):

INSTALLED_APPS = [
        "myapp",
        "myapp.virtual_reality",
        "myapp.augmented_reality",
        ...
]

拥抱水平模块化

如果事实上没有明显的方法将一个非常大的应用按子特性细分成垂直分段的子组件,你总是可以依靠“水平”分段。同样,这意味着按照与业务需求或特性(垂直)相反编程工具来组织代码。

myapp/
        forms/
                ...
        models/
                __init__.py
                augmented_reality_models.py
                core_models.py
                virtual_reality_models.py
        ...

如果没有别的,这种模式比不存在明确的业务特征划分的垂直细分更好。

然而,对于大多数新的独立应用来说,所有这些问题更多的是假设而非现实。

摘要

在这一章中,你学习了模块化在你的独立应用中的重要性,以及不同的代码组织模式对于代码重用和其他开发者的易读性的影响。在下一章中,我们将回到打包的问题,并学习如何更好地跟踪包版本,确保您的测试针对可安装的代码运行,并配置您的项目以创建包索引就绪的版本。

十七、更好的打包

在第八章中,我们使用了一个 Django 应用,并创建了一个简单的 Python 包来分发这个应用。我们在这里追求的是更简单的包配置代码,也就是说,更容易阅读和更新,以及最大限度地保证我们测试的就是我们发布的。

在这一章中,我们重温了从第八章开始的包,并探索了一些改进我们所建立的包的方法,以便包含额外的信息并使更新这些信息更容易。

版本整合

我们在第八章的 setup.py 文件看起来像这样:

from setuptools import setup, find_packages

setup(
    name="blog",
    version="0.1.0",
    author="Ben Lopatin",
    author_email="ben@benlopatin.com",
    url="http://www.django-standalone-apps.com",
    packages=find_packages(exclude=["tests"]),
)

软件包版本在这里被指定为安装文件中的一个字符串。我们需要此处包含的版本,以便通知特定版本的包索引。使用我们这里的字符串文字的问题是,你最终会得到一个重复出现的字符串。如果您将版本包含在您的软件包本身中,正如您应该做的那样,那么您有两个地方需要在每次更新发行版时更新版本。

  • 在包中定义版本的好处是,例如,在 init 中设置一个变量。py 文件的一个优点是它总是可以用来验证来自其他包的版本。例如,打开一个 Python 控制台,导入包,检查 myapp 的值是多少,这很简单。__ 版本 _ _ 是。

解决方案的关键是将版本包含在一个规范的位置,并在其他地方重用它。有几种方法可以做到这一点,但最终都依赖于对模块根的处理——即 init。py 或一个专门的文件——作为事实的来源。

最明显的策略是简单地在 init 中声明版本。像这样的 py 文件

__version__ = "2.4.0"

然后在 setup.py 文件中导入该包

from setuptools import setup, find_packages

import myapp

setup(
    name="blog",
    version=myapp.__version__,
    author="Ben Lopatin",
    author_email="ben@benlopatin.com",
    url="http://www.django-standalone-apps.com",
    packages=find_packages(exclude=["tests"]),
)

这是一个吸引人的策略,但也是应该避免的。在安装之前导入正在安装的软件包可能会在安装过程中造成问题,尤其是如果您的应用指定了仅由您的应用安装的任何依赖项。另一种方法是只为包元数据使用一个单独的模块,从这个模块导入版本是安全的。姑且称之为 meta。py:

__version__ = "2.4.0"
__author__ = "Ben Lopatin"

您的 init。py 文件可以从这个 meta 中导入值。py 文件,您的 setup.py 也可以,没有风险或导入未安装的依赖项。

from setuptools import setup, find_packages

import myapp.__meta__

setup(
    name="blog",
    version=myapp.__meta__.__version__,
    author=myapp.__meta__.__author__,
    author_email="ben@benlopatin.com",
    url="http://www.django-standalone-apps.com",
    packages=find_packages(exclude=["tests"]),
)

导入这些值的一个经过验证的替代方法是读取和解析文件,甚至不需要将它导入名称空间。这个策略的价值很快就会显现出来。

from setuptools import setup, find_packages

with open("myapp/__init__.py", "r") as module_file:
    for line in module_file:
        if line.startswith("__version__"):
            version_string = line.split("=")[1]
            version = version_string.strip().replace("\"", "")

setup(
    name="blog",
    version=version,
    author="Ben Lopatin",
    author_email="ben@benlopatin.com",
    url="http://www.django-standalone-apps.com",
    packages=find_packages(exclude=["tests"]),
)

这不存在导入模块的风险,即使代码本身还不可导入时也可以这样做。

使用源目录

在我们的基本包示例(第章第八部分)中,源目录如下所示:

blog_app
├── blog/
├── ...
├── manage.py
├── runtests.py
├── setup.py
|── tests/
|── ...

其中 blog/代表代码的包目录。安装后,博客的打包内容将可以使用导入博客。这是打包 Python 应用的最自然的方式,但是它有一个明显的缺点。

  • 您的测试不会针对包运行,因为它将由其用户安装。它们与您的项目目录中的任何情况背道而驰。 1

不管您使用什么样的代码布局,您可能会遇到的一个问题是,您可能最终会对与您发布到包索引中的代码不同的代码运行您的测试。这可能是因为您尚未提交对本地存储库的更改,或者存储库中不包含文件。这个问题很容易通过使用持续集成系统自动运行测试来解决。

目录布局所带来的问题非常相似,但又有所不同。有可能对已发布的存储库中存在的完全相同的文件运行测试,但却遗漏了已部署代码中的错误,因为您的软件包目录中的内容不一定是软件包安装的内容!根据您如何定义 packages 参数和您在 MANIFEST.in 文件中定义的内容,您可能会在已安装的版本中得到不同的(即缺失的)源代码。

将这个源代码放在您的 src/目录中的目的是,它只对已安装的代码进行测试,以减少您发布一个损坏或不完整的包的可能性。

blog_app
├── src/
├────blog/
├── manage.py
├── runtests.py
├── setup.py
|── tests/

这是由几个因素造成的。首先是 src/目录不是 Python 模块。它只包括您的代码包,不包括它自己的 init。py 文件。这排除了直接从包目录导入。第二,测试在它们自己的顶层模块中,而不是位于包目录中。这将强制对已安装的包运行测试,而不是对目录中的代码运行测试。

尽管有软件包发布的好处,但是将代码移动到一个单独的目录会带来一些小的挑战。首先,您不能再直接运行您的测试!您的应用代码不再位于您的 Python 路径中。使用 tox 或 nox 在隔离的特定于测试的虚拟环境中进行测试解决了几个问题,包括允许您在测试运行中隔离地重新安装应用。更直接但不太可靠的策略是将 src/目录添加到您的路径中。

PYTHONPATH=src/ pytest tests

这种方法便于开发,但是不应该依赖于发行版,因为它避开了通过将代码移动到 src/目录所提供的保护。

使用 Django 独立应用移动我们的代码的挑战之一是,我们希望使用代码来创建包含在源代码和包中的工件。如果我们想使用 manage.py 脚本为独立应用创建迁移,我们会遇到相同的就地测试问题。幸运的是,这可以通过使用与测试类似的策略来解决。这里使用简单的路径修改命令更有意义:

PYTHONPATH=src/ ./manage.py makemigrations myapp

创建迁移(以及任何其他特定于应用的任务)也可以封装在 tox 环境或 nox 会话中,这只是为了方便或确保此类任务针对已安装的软件包运行:

@nox.session
def migrate_on_path(session):
    session.install("-r", "requirements-test.txt")
    env = {"PYTHONPATH": "src/"}
    session.run("python", "manage.py", "check", env=env)
    session.run(
                "python", "manage.py", "makemigrations", "myapp", env=env)

@nox.session
def migrate_from_installed(session):
        session.install("-e", ".")
    session.run("python", "manage.py", "check", env=env)
    session.run(
                "python", "manage.py", "makemigrations", "myapp", env=env)

这些“会话”中的每一个都将在它自己的隔离虚拟环境中运行。第一个将运行 check 命令,并按照 src/目录中的布局针对源构建迁移。这个会话需要安装任何在你安装应用时添加的或者预期要安装的需求,例如 Django 本身。第二个 nox“会话”安装应用,然后针对已安装的软件包执行命令。

使用 setup.cfg

从 setup.py 文件中删除字符串形式的版本是一种质量改进,减少了包中出现版本错误的可能性。在您的包配置中还可以做一些额外的改进,使它更容易阅读和更新。

不需要在 setup.py 文件中将所有元数据作为参数提供给 setup 函数,而是可以将它们添加到可读性更好的 ini 格式的 setup.cfg 文件中。除了可读性之外,这样做还有几个好处。一是该文件可用于其他工具的元数据(如林挺工具),二是它提供了从模块属性中提取版本的本地策略。前提是 version 是版本标识符,并且是在 init 中定义或导入的。py 文件,以下示例 setup.cfg 文件将充分替换 setup.py 文件中的元数据定义:

[metadata]
name = blog
version = attr: myapp.__version__
author = Ben Lopatin
author_email = ben@benlopatin.com
url = http://www.django-standalone-apps.com

[options]
packages = find:

[options.packages.find]
where = src

尽管 setuptools 仍然需要 setup.py 文件来构建您的软件包,但是现在可以将您的 setup.py 文件简化为以下内容:

from setuptools import setup
setup()

这增加了一个文件和一些额外的代码行;然而,结果可能更容易理解,由于 setuptools 内置了用于读取版本属性和加载文件(如您的自述文件)以填充描述字段的启示,这可能是一种更简单的配置格式。

pyproject.toml 和更多工具

为了结束这一章,我们将添加另一个配置文件,然后看一下如何使用它来完全替换 setup.py 和 setup . CFG。PEP 518,“指定 Python 项目的最低构建系统要求”, 2 指定一个顶级 TOML 文件,该文件可用于定义构建所讨论的包(即您的独立 Django 应用)所需的包。

  • TOML,“Tom 的显而易见的,最小的语言”,是一种指定的,类似 INI 的配置语言,允许嵌套。

pyproject.toml 文件是一个顶级文件,具有 PEP 指定的格式,是一个与工具无关的文件(contra setup.py ),也可以被各种开发工具重用。

文档中的示例文件代表了需要包含的基本文件:用于构建包的 setuptools 和用于构建 wheel 档案的 wheel:

[build-system]
requires = ["setuptools", "wheel"]

然而,PEP 518 还指定了可定制的(工具)头,其中可以为各种开发工具添加配置,包括构建和测试(注意,这种支持也完全依赖于工具本身)。这允许替代的构建系统使用 pyproject.toml 文件作为构建指令和包元数据的来源。

一个这样的工具是诗歌,通过使用它来构建您的 Django 独立应用——或任何 Python 项目——您可以完全依赖 pyproject.toml 文件,而无需 setup.py 或 setup.cfg 文件。这里有一个简短的例子,包括构建项目和开发所必需的包元数据和独立的依赖定义。这也排除了对一个或多个 pip 需求文件的需求,因为依赖关系定义被用来创建一个“锁”文件,该文件具有由诗歌解析的精确固定版本以实现版本兼容性。

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

[tool.poetry]
name = "myapp"
version = "2.5.0"
description = "Support for multi user accounts"
authors = ["Ben Lopatin <ben@benlopatin.com>"]
license = "MIT"
packages = [
    { include = "myapp", from = "src" },
]

[tool.poetry.dependencies]
python = "³.5"
Django = "³.0"

[tool.poetry.dev-dependencies]
pytest = "~5.0"
pytest-django = "~3.7.0"

清晰、版本管理和文件合并的好处使得使用像 poem 这样的工具成为使用 setup.py 和 setuptools 的一个诱人的替代品。然而,值得考虑一些潜在的不利因素。没有简单的方法在您的包中提供一个规范的版本,这意味着您将需要冗余的版本声明。用 Python 编写的更复杂的构建过程可能不会受益于或有助于声明性配置。项目本身仍然相对较新,主要由一个开发商驱动,这意味着项目的“公共汽车因素”非常小。话虽如此,只要替代品存在, 3 的锁定成本就很小。

摘要

在本章中,您学习了如何通过为包版本创建一个单一的源来防止由于重复而导致的错误,通过使用一个单独的源目录来确保测试在安装时针对包运行,以及通过依赖附加文件(如 setup.cfg 和 pyproject.toml)来构建 Python wheel 包并简化构建要求,来改善您打包独立 Django 应用的体验。

在下一章中,您将了解独立应用的许可,包括软件许可提供的内容以及如何包含它们。

Footnotes 1

Hynek Schlawack,hynek . me/articles/testing-packaging/

  2

www . python . org/dev/peps/pep-0518/

  3

包括 setuptools 和 Flit,另一个以 pyproject.toml 为中心的构建工具

 

十八、协议

作为开源软件的用户,大多数开发者可能认为我们使用的软件的版权和许可条件是理所当然的。当你发布自己的软件时,这就有点难了。如果您需要在自己的软件中包含其他软件,事情可能会变得更加复杂。

  • 作者注:本章中的任何内容都不应被理解为法律建议。如果您对许可或使用许可软件有法律顾虑,您应该寻求专业的法律建议。

许可证有什么作用

许可证做的第一件事是明确声明软件的版权所有。在世界上大多数司法管辖区,仅仅通过创作新作品的行为就授予版权。许多国家提供了注册版权的方法,但这一步不是必需的;版权是自动的。然而,拥有版权和主张版权是两回事。

第二,许可证是一种协议。它们是软件创作者和用户之间的条款协议。在 web 应用的情况下,用户可能是开发人员或部署软件的任何人(例如,企业)。包含的条款多种多样,但协议的机制通常是以这种或那种方式使用软件。

许可证的条款可能很严格,比如那些包含在普通商业许可证中的条款。有些许可证可能禁止复制或重新分发软件、修改软件,甚至禁止以某种方式使用软件。其他许可证,如通用开源许可证,可以自由地允许您对软件做任何您想做的事情,只要您将许可证及其版权和条款包括在内。这些术语是权利和义务的某种组合。

在基本层面上,大多数开源许可证的条款否认使用该软件的任何担保或责任。毕竟,它是免费提供源代码的。这似乎没有必要,尤其是在 Django 独立应用的环境中。然而,鉴于在一个潜在的诉讼社会中,否认与陌生人分享的任何责任的成本接近于零,这是一个不错的选择。

除此之外,如果你的目的是与全世界免费分享你的独立应用,那么似乎没有必要声明任何条款(有专门为此提供的许可证!).然而,许可证的一个关键方面是它明确允许任何人用它做他们想做的事情。这意味着其他人不能为自己主张你的软件,然后规定不同的条款。

许可证的种类

目前使用的许可证有很多种,但它们分为几类:

  1. 商业许可证

  2. 开源许可证

  3. 公共领域许可证

商业许可证是由付费用户颁布的,并且只能由付费用户颁布。例如,微软视窗和苹果 macOS 是商业许可软件。这些通常不允许任何类型的修改或重新发布,并且通常无法访问源代码。

开放源码许可是多种多样的,但它们是统一的,因为它们提供对源代码的访问。大多数使用开源许可的软件都可以自由修改和重新发布,尽管超出这一范围的条款可能会有很大的不同。这些术语可以大致分为两类:(1)许可的和(2)左版权的。

“许可”许可证是一种简单的非版权开源许可证,它保证使用、修改和再分发的自由,但允许专有的衍生作品。

—开源倡议

许可许可证包括 MIT、BSD 和 Apache 许可证。这些许可证很大程度上授予用户以他们认为合适的方式使用软件的自由权利。如果愿意,他们可以在封闭源代码和商业许可的软件发行版中重新打包它。唯一的要求是他们要把你的执照给我。

“copy left”是指允许衍生作品但要求其使用与原作相同的许可的许可。

—开源倡议

copyright left 许可证包括 GPL 或 GNU 公共许可证的变体。这些许可证不仅要求其许可证被传递,而且要求该许可证的条款适用于使用它的任何其他软件。左版权许可的关键激活因素是源代码的修改。

公共领域许可证本质上是反许可证。他们不主张对源代码的任何权利或限制如何使用源代码的能力。它们也不授予任何权利或许可,因此实际上并不像它们在哲学上那样有吸引力。

你应该选择哪个?与(a)选择一个许可证并将其包含在您的独立应用中以及(b)选择一个预先存在的许可证相比,您为自己的一个或多个项目选择的特定许可证并不重要。前者会给其他人信心,让他们可以使用你的软件,如果需要的话,可以修改你的软件。后者确保你有一个别人能理解和认可的许可证。

如何以及在哪里包含您的许可证

有几个明显的地方可以包含您的许可证,这取决于您在任何给定的地方包含了多少许可证。这可以包括

  • 许可证标识符,例如,“MIT”

  • 许可证摘要

  • 整个许可证

至少,您应该在顶级许可证文件中包含完整的许可证和版权声明。这确保了你检查了每一个框,并且这是一个期望找到它的地方。

许可证的下一个位置在 setup.py 文件中,使用 setup()函数的 license 参数来标识许可证。这确保了包索引上的信息是立即可用的,以高度可见和可搜索的方式。

你还想把它放在哪里?

  • 在您的自述文件中,至少要一目了然地识别许可证

  • 例如,在根模块中,init。py,使用 dunder 值,如 license

  • 在您的项目文档中

  • 在单独的 Python 文件本身中

至于在单个 Python 文件中包含许可证,这是不必要的。在企业赞助的开源项目中,这是一种常见的做法,它非常清楚谁拥有版权以及许可证需要什么。对任何其他开发者来说,主要的好处是它使人们更容易使用你的代码的摘录,例如,出售一个单独的模块,包括你的版权和你的许可。

如何包含其他许可证

在某些情况下,您可能希望或需要在您的独立应用中直接包含其他预先存在的软件,无论这是“出售”整个软件包还是仅包含单个模块。如果这样做,您必须首先确保该软件的前身许可证允许这种分发。接下来,您必须确保分发应用的许可证与前任许可证兼容。最后,您必须包括前任许可证。

第一个问题真的是前任软件有没有许可证,是不是开源许可证。无论使用哪种许可证,它都应该明确允许软件的再分发和修改(如果只使用一部分)。如果前身许可证是一种常见的许可证,比如 MIT 或 GPL 许可证,那么这个问题的答案就相当明显了。对于自定义或“虚荣心”许可证,您可能需要做一些研究。

第二个问题与前任许可证授予和要求的具体权利和义务有关。有些许可证,如麻省理工学院许可证,只要包含原始许可证,对软件如何重用没有义务。其他的,比如 GPL 的变体,要求任何再分发都必须以同样的方式获得许可。因此,如果你想使用 GPL 许可你的独立应用,并包含一些麻省理工学院许可的软件,这将是可行的,当然,前提是你要明确哪个许可包含哪些组件。另一方面,如果你想使用 MIT 许可证来许可你的独立应用,你将不能在你的发行版中包含 GPL 许可的软件。需要澄清的是,这并不意味着你的应用不能使用不兼容许可包的软件,只是你不能将软件包含在你发布的内容中。通过可安装包重用是好的。

第三个也是最后一个问题是如何包含前任许可。即使前任软件与您的应用使用相同的许可证,您仍必须包括前任许可证。这不仅包括软件许可条款,还包括版权所有权。一个好的起点是您的顶级许可文件。在这里,您可以在自己的软件后面附加上与该软件相关的组件的版权声明。在许多情况下,这就足够了。

如果你使用一个单独的模块,你应该在模块中直接包含许可证或者至少是它的缩写引用。为此,使用代码注释是一个好主意;使用模块 docstring 可能会混淆 how to 文档和许可声明。

# -*- coding: utf-8 -*-
# COPYRIGHT (c) Some Other Developer
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of package.

对于较短的许可证,习惯上在这样的注释中重申整个许可证,但是如果你包括了许可证并且使引用变得明显,你不需要这样做。如果您出售的是一个软件包,包括整个软件包,那么您可以依赖模块本身中包含的许可,尽管如此,也可以将原始的许可文件包含到软件包目录中。

摘要

在这一章中,我们回顾了什么是软件许可,它们对您和您的独立应用的用户有什么作用。我们还研究了作为软件作者的您可以使用的许可应用的一些选项、所涉及的权衡、在包含其他软件时如何处理许可,以及在您的独立应用中包含许可信息本身的一些明智策略。

在下一章中,我们将了解如何发布新版本的应用,这一过程中需要什么,以及如何简化它。

十九、记录您的独立应用

一旦你向全世界发布了你的独立应用,其他人就会想要使用它。他们将如何使用它?他们将如何安装它?它能解决他们遇到的同类问题吗?在这一章中,我们将看看如何开始解决记录你的独立应用的挑战。

从问题开始

不管你用什么格式和工具来记录你的独立应用,或者你决定如何在你的代码库中分发文档,你所有的文档都应该以回答别人——甚至是未来的你——在处理你的独立应用时会遇到的问题为动机。

你应该问的几个问题是谁将会阅读这篇文章他们正在试图做什么。最常见的“谁”是开发者用户,他们要么评估是否使用你的独立应用,要么寻求帮助将其集成到他们的项目中。但也可能是其他人,另一个试图评估你的独立应用是否应该被视为项目选项之一的项目利益相关者。

有人试图做的“什么”更重要,也更容易分解。让我们将这些问题分类如下:

  1. 这个独立的应用适合我的项目吗?

  2. 我如何开始使用它?

  3. 它完全有能力做什么?

  4. 我如何报告错误或参与进来?

第一个问题是关于考核。我所说的评估并不是指对你的应用进行排名或打分,而是判断某人是否应该使用它。这是人们在查看你的独立应用时会遇到的最广泛、最主观的问题,而且没有单一的、万无一失的方法来解决这个问题。

然而,有几个关键问题你可以也应该解决,这将有助于人们评估你的应用,包括(I)回答它解决什么问题,(ii)它如何解决那个问题,和/或(iii)它解决这个问题的方式与其他解决方案有何不同。

一个简单的例子来自独立应用 django-rq :

  • Django 与 RQ 的集成,RQ 是一个基于 Redis 的 Python 队列库。Django-RQ 是一个简单的应用,允许您在 Django 的 settings.py 中配置队列,并在您的项目中轻松使用它们。

我们现在确切地知道这是做什么的。我们还不知道如何安装或使用它,但从这两句话中,你可能会对这个应用是否会对你有所帮助有一个相当好的想法。

一旦有人决定使用你的应用,他们接下来会想知道如何开始使用它。这包括(I)安装,(ii)配置,以及(iii)任何后续的集成或使用步骤。

安装通常很简单,包括 pip 安装和将应用添加到 INSTALLED_APPS,但是您应该确保明确这里使用的名称,并记录与预期的任何差异。至于配置,至少您需要包含对项目设置(除 INSTALLED_APPS 之外)的任何更改或添加,例如中间件添加,以及对项目 urls.py 配置的添加。

集成和使用包括使用您的独立应用所需的用户自己代码的更改,以及用户应该知道的任何命令或输出。如果你的独立应用包含了视图的 mixin 类,用户应该立即意识到什么属性或方法?如果它包括管理命令,它们的名称是什么,需要哪些参数?你可以包括更多的内容,但是对于即时的回答,像这样的问题将指导你应该包括什么。

最后是用户如何提供反馈或对项目做出贡献的问题。人们很容易将这些问题视为理所当然,并认为答案是不言而喻的——当然只是在回购上制造一个新问题!–但情况并非总是如此,你应该向你的用户说明这一点。一个简单的关于向哪里提交问题或是否有电子邮件提问的声明就足够了,但你可以也应该包括更多的指导,不仅包括在哪里记录问题,还包括如何记录问题。这将节省你的用户和你的时间。

文件的形式

文档应该从自述文件开始。这是一个带有或不带有基本标记格式的单个顶级文本文件(例如,README.rst 或 README.md)。它描述了您的应用的功能以及如何安装和配置您的项目以使用它,并且包括一些基本的使用文档或指向可以找到它的位置。当人们在公共存储库中看到您的应用时,通常会首先看到这个文件,并且很容易将这里的内容包含在 setuptools 用来包含在 PyPI 中的 long_description 中。这也是一个长期运行的约定,所以无论您将文档的其余部分放在哪里,或者您如何组织它们,有人都可以在源根目录下找到这个文件。

除此之外,它有助于在单独的文件或部分中开始您的文档,以解决使用您的应用时的需求层次(图 19-1 )。

img/486718_1_En_19_Fig1_HTML.jpg

图 19-1

马斯洛的需求层次,维基百科用户 FireflySixtySeven (CC BY-SA 4.0)举例说明

这种需求层次结构模仿了马斯洛流行的需求层次结构,旨在说明人类需求的层次结构,即在满足上述需求之前,必须满足最低层次的需求。不管心理学理论是否正确,它都是接近文档的有用类比。

在基础级别,安装和配置是第步。在不知道如何实际获得你的应用以及需要转动什么“转盘”的情况下,所有后续的文档都没有什么实用价值。通常自述文件本身就足以满足这一目的,但是如果可用的设置多于几个,那么安装和配置,或者仅仅是配置,可能需要在文档中有它们自己的章节。

接下来是上一节描述的基本用法和集成。思考这个问题的一个简便方法是快速开始使用你的应用,而一个 quickstart 是展示如何实际使用你的应用的一个很好的方式,而不需要钻研完整的文档。这通常是开始使用应用的最低要求,从最常用的命令到将你的应用集成到另一个应用的简单实用的例子。

对于需要非平凡集成的应用,比如构建模块或者甚至额外的框架,一个关于这样做的教程是有价值的下一步。教程通过示例指导用户使用软件的步骤,每个步骤都有助于实现明确定义的最终目标。这对于许多独立的应用来说是不必要的,但是对于具有重要功能的应用来说,这对于显示如何在快速启动不够的地方使用应用是有用的。

例如, django-graphene 应用,一个用于向 django 项目添加 GraphQL 功能的独立应用,在其文档中使用了两个这样的教程。基础教程指导用户完成典型的快速入门步骤,然后在示例应用中创建示例模型和视图,甚至加载提供的测试数据以匹配指定的模型。从这里,用户可以通过构建一个小应用来实际了解应用的工作原理,并将文档中描述的结果与他们在自己的计算机上看到的结果进行比较。

教程提供了一种“横向”的方法来记录一个独立的应用,从一个问题开始,该问题的示例解决方案包括功能的许多不同方面, API 参考提供了一个“纵向”的文档来源,其中的细节是通过实现来组织的。Django 文档很大程度上是这样组织的。Django 文档并没有包含每种问题的教程,而是包含了一个基本的教程,然后是按照功能逻辑组织的详细文档。没有关于如何创建一个在电子邮件报告中发送汇总图书排名信息的应用的教程,但有关于如何使用电子邮件、如何使用 Django 的模型类以及各种数据库聚合和表达式的详细文档。

最后,在我们层次的顶端,是食谱。顾名思义,cookbook documentation 是一个食谱的集合,是真实世界中的小例子,既展示了如何使用你的独立应用的功能,又提供了如何应用它的灵感。这些可以从真实的用例中提取,也可以是虚构的,尽管不管是否虚构,它们都应该是有用的。

代码注释和文档字符串

许多 Python 程序员小心翼翼地记录他们的代码,包括对“有趣的”代码块的注释;解释模块、类和函数的详细文档字符串;甚至键入提示。编写清晰、文档完善的代码对于开发来说是一个极好的帮助,尤其是对于新的贡献者,对于那些只想更好地理解你的应用内部的开发者用户来说是一笔财富。也就是说,代码文档和用法文档是有区别的。

原因不仅包括如何阅读文档(参见“文档工具”一节),还包括组织和详细程度。简而言之,当这些问题与应用正在解决的问题有关时,源代码通常不会被组织来回答为什么如何的问题;它是为了解决问题而组织的。入口点是为执行和导入而设计的,而不是为阅读和浏览而设计的。将文档与源代码分离也是有价值的,因为这种分离使得对文档的贡献更加容易。

小心不要把代码文档误认为用户或项目文档。

文档工具

一旦你有了独立应用的文档,下一步就是让潜在用户无需查看源代码就可以阅读。无论您使用的是 reStructuredText 还是 Markdown,都有一些工具可以让您轻松地将文档转换成 web 可部署的 HTML。

最常用的工具是 Sphinx,它主要是为处理 reStructuredText 文档而设计的,尽管它也支持 Markdown。如果您曾经阅读过 Python 或 Django 文档,那么您已经通读了 Sphinx 生成的文档。这是一个强大的工具,但是入门很简单。要安装它并创建初始配置,请在控制台中从 docs 源目录运行以下命令:

pip install Sphinx
sphinx-quickstart

sphinx-quickstart 命令将引导您完成几个提示,以帮助您确定在何处包含构建的 HTML、基本项目信息和编写文档所用的自然语言。生成的 Makefile——或者 make.bat,如果您使用的是 Windows——可以用来读取重构的文本源文件,并创建可浏览和可搜索的 HTML 文件。您也可以构建其他格式,包括 PDF,尽管这可能需要额外的软件,如 LaTeX。

使用 Sphinx 和 reStructuredText 构造文档的细节超出了我们在这里讨论的范围;然而,reStructuredText 允许您创建丰富的索引,还可以从独立应用的源代码构建文档。

对于那些强烈倾向于使用 Markdown 的人来说,Sphinx 的替代方案是 MkDocs。Markdown 和 MkDocs 在功能上的不足,在简单性上得到了弥补。MkDocs 项目是使用 Yaml 文件而不是 Python 来配置的,与 reStructuredText 相比,Markdown 的语法以及功能更加精简。如果你和其他已经熟悉降价的人一起工作,这可能是一种优势。就像 Sphinx 一样,MkDocs 将获取您的文档源并创建可浏览、可搜索的 HTML 文档。

一旦有了可以将文档源转换成 HTML 的工具,就差不多可以部署它了,这样开发人员用户就可以在线浏览文档了。最简单的方法是通过 web 提供构建的 HTML,例如,将其复制到 Web 服务器,添加到存储库分支以使用 GitHub 页面等。虽然这种方法可行,但它确实引入了大量的手动工作。

相反,您可以使用阅读文档来自动构建和托管您的项目文档。read Docs 将与 Sphinx 或 MkDocs 一起工作,如果您使用 GitHub、Bitbucket 或 GitLab,它将允许您使用项目集成来连接您的存储库,以从存储库更新进行构建(您也可以将它用于其他源代码平台;但是,这将需要更多的手动设置)。

摘要

在本章中,您已经学习了如何开始为您的独立应用编写面向用户的文档,包括在指导它时要问什么样的问题、适合用户的文档形式以及实际部署文档的工具。

在下一章中,我们将深入研究测试中的其他主题,包括测试迁移以及如何测试不同版本的 Python 和 Django。

二十、附加测试

一旦你在你的项目之外成功地测试了你的独立应用,看起来你已经完全完成了测试。然而,还有很多你无法测试和防范的东西,随着越来越多的人使用你的应用并决定做出贡献,这变得尤为重要。

在这一章中,你将学习如何测试典型单元测试没有发现的 bug,如何测试多个版本的 Python 和 Django,以及作为 Django 测试运行程序替代的 pytest 测试框架。

测试迁移

您可能希望测试数据库迁移的几个方面,包括模式更改本身、数据迁移的准确性以及是否有任何未迁移的更改。

为了测试模式迁移,为您的模型准备好测试就足够了。对于这些变化,您通常不需要任何特殊的测试。如果您的迁移包括重要的数据迁移,您可能希望测试这些迁移是否按预期填充或修改了应用数据。如果是这种情况,您将想要测试特定的迁移或迁移函数本身(即,由迁移文件中的operations.RunPython运行的代码)。如果您将整个迁移作为一个完整的单元进行测试,这就需要管理迁移流程,这可以通过修改 TestCase 类或使用专门构建的库(如django-test-migrations)来完成。在数据迁移使用具体模型的情况下——由于缺少时间点模型属性,这通常被认为是一种反模式——您通常可以将单个迁移功能视为一个单元并直接进行测试。这包括在初始阶段按照预期设置一些测试数据,执行迁移功能,并验证测试数据库中的数据现在是否与预期的结果相匹配。

当谈到未迁移的变更时,我们想要发现的是是否有任何未完成的模型变更会导致新的数据库迁移,并将这种情况视为错误。这样做有两个原因。首先,您可能会面临发布实际上不完整的模型状态的风险。例如,如果您已经更改了模型字段的允许状态,使得它不再可以为空,并且您的测试数据填充了该字段,那么您可能不会注意到迁移丢失,从而导致当其他人部署应用的更新版本时出现问题。第二个原因是,您将最终在运行 makemigrations 的最终用户为您的应用进行他们自己的新迁移的状态下交付应用,这很可能与随您的应用一起交付的后续迁移相冲突(我自己也这样做过,事实证明非常烦人!).

与这两个原因相关,完全有可能您已经完成了所有必要的迁移,但是意外地没有将它们提交到您的源代码控制存储库中。结果,您的测试将在本地通过,但是您将在损坏的状态下交付它。进行测试意味着您可以在一个持续集成的系统中测试缺失的迁移,该系统只对您提交和推送的内容起作用。

针对不同版本进行测试

比测试迁移更令人兴奋和重要的是能够明智地测试 Python 和 Django 的多个版本。测试不同版本的 Python 和 Django 有几个原因。最明显的是,如果你发布你的独立应用给其他人使用,你不能假设其他人都在使用相同版本的 Python 或 Django。对于您的应用支持的每一个 Django 版本,都有一组支持的 Python 版本,该版本的 Django 将在这些版本上运行。不能保证支持给定 Django 版本的应用或库也支持所有相关的 Python 版本;然而,这是一个非常合理的期望,它将支持所有相关的版本。

其次,针对多个版本的 Python 和 Django 进行测试,可以让你的应用更容易适应未来。您可以继续构建支持您当前选择的环境的版本,同时确保与 Python 和 Django 新版本的兼容性,甚至在它们正式发布之前。

有几个工具可以让你做到这一点,包括持续集成服务和本地工具,如 tox 和 nox。我们之前在第十五章中介绍了如何使用 tox。nox 是一个有点类似的工具,就像 tox 一样,它将创建、管理和使用单独的特定于测试的虚拟环境来运行您的测试。然而,与 tox 不同,它使用基于 Python 的配置。这允许你做像链“会话”这样的事情,任务相关块 nox 让你写运行测试和其他任务。

下面是从 django-organizations 的一个正在进行的分支中提取的一个功能齐全的 nox 配置文件(noxfile.py ):

import nox

pytest = '4.4.1'

@nox.session(python=['3.6'])
@nox.parametrize('django', ['1.11', '2.0'])
def tests(session, django):
    session.install(f'pytest=={pytest}')
    session.install(f'Django=={django}')
    session.install('-r', 'requirements-test.txt')
    session.install('-e', '.')
    session.run('pytest')

为什么用 nox 代替 tox?主要原因是个人偏好基于可组合 Python 的配置,而不是类似 ini 文件的配置格式。一个更有说服力的理由是能够使用它来运行测试和执行非测试命令,如构建或发布,从而整合 tox 和 Makefile 的用例。

使用 pytest

Django 默认使用 Python 标准库的 unittest 测试框架。捆绑的测试用例类是基于 unittest 的。TestCase 和测试运行程序也在根目录下。然而,unittest 库并不是测试 Python 代码的唯一方法,pytest 是一种越来越流行的替代方法。

与 unittest 库相比,pytest 在编写和运行测试方面有一些主要优势。首先,pytest 允许您使用单独的函数编写测试,而不需要创建整个类。第二,pytest 使用内置的 assert 语句进行比较,因此不需要使用像 assertEqual 这样的方法。例如,给定这个表单类

from django import forms
from dateutil.relativedelta import relativedelta

class AgeValidity(forms.Form):
        birthdate = forms.DateField()

        def clean_birthdate(self):
                dob = self.cleaned_data["birthdate"]
                if dob + relativedelta(years=18) < date.today()
                        raise forms.ValidationError("Min. age not met")
                return dob

您可以在一个函数中编写一个基本的验证检查:

import datetime
from dateutil.relativedelta import relativedelta

def test_form_date_validity():
        given_date = date.today() - relativedelta(years=18, days=-1)
        form = AgeValidity(data={"birthdate": given_date})
        assert not form.is_valid()

当然,这是一个简单的测试,但是除了测试类中的测试方法之外,它不需要任何东西,然而测试类不需要被编写。这些功能本身就很方便。更确切地说,是可组合的测试夹具、测试运行和插件生态系统的结合使它成为一个引人注目的选择。

不用在每个测试用例类的 setUp 或 setUpTestData 中创建测试数据的实例,您可以创建单独的和可重用的函数来返回(或产生)您的测试数据。pytest 然后将这些与测试函数参数名称匹配,并传递数据,而不需要显式的 import 语句。这里有两个 pytest fixtures 生成测试数据的生成器——其中一个依赖于另一个。

@pytest.fixture
def account_user():
    yield User.objects.create(
                username="183jkjd", email="akjdkj@kjdk.com")

@pytest.fixture
def account_account(account_user):
    vendor = create_organization(
                account_user, "Acme", org_model=Account)
    yield vendor

然后,通过命名参数以匹配夹具,可以在任何测试中使用这些参数:

def test_invite_returns_invitation(
        account_user,
        account_account,
):
        backend = ModelInvitation(org_model=Account)
        invitation = backend.invite_by_email(
                "bob@newuser.com",
                user=account_user,
                organization=account_account)
    assert isinstance(invitation, OrganizationInvitationBase)

您的测试套件的累积好处是减少了测试中的设置和拆卸,减少了创建必要的测试数据所花费的精力。

为了像这样使用 pytest 来测试您的独立应用,您很可能需要使用 pytest-django 插件。这使得与数据库的交互变得轻而易举,并且附带了一些 fixture(或者 fixture generators,如果你愿意这样想的话)用于典型的 Django 相关测试,比如访问视图的测试客户机。使用 pytest-django 将测试标记为可以访问数据库,下面是一个 pytest 测试函数,用于验证没有未创建的迁移:

@pytest.mark.django_db
def test_no_missing_migrations():
    call_command("makemigrations", check=True, dry_run=True)

有理由不使用 pytest。首先,您的基于单元测试的测试套件可能对您来说很好。在测试运行期间隐式加载设备是很方便的,但是会混淆设备的来源,并且这个特性的魔力可能没有吸引力。然而,对于小型独立应用,它可以使开始编写测试更容易,对于非常大的独立应用,它可以使重用测试数据和运行独特的、特定于功能的测试集更容易。

摘要

在这一章中,你已经学习了在测试过程中验证迁移,使用不同版本的 Python 和 Django 进行测试的工具,以及使用 pytest 作为替代测试框架。在下一章中,我们将通过自动化将所有这些联系在一起,添加本地和远程执行的过程,为您简化开发过程,并为使其他开发人员也能做出贡献提供良好的基础。

二十一、自动化

在前一章中,我们通过测试不同版本的 Python 和 Django,以及替代的测试运行程序,扩展了测试独立应用的能力。您了解了如何使用 tox 或 nox 来测试不同的版本,以及如何测试您的迁移,并简要介绍了 pytest 测试框架。

在这一章中,我们将看看如何通过尽可能自动化来超越这一点,这不仅是为了我们的方便,也是为了帮助提高我们的应用的质量和其他贡献开发者的共享体验。

这是什么,为什么这么麻烦?

有许多方法可以定义自动化,但是为了我们的目的,我们将从这个定义开始:它是任何不需要后续干预而对其他过程进行排序的过程。我们在这里强调某种父流程是因为某事某人仍然需要启动该流程,此外,自动化不是只有机器人或某种基于云的系统才能满足的事情。毕竟,测试自动化可以通过在您自己的计算机上运行命令来执行。使它自动化的是,整个测试套件可以只通过一个命令来运行。

因此,如果“自动化”听起来令人生畏,那么从替换“脚本”开始,你将获得 80%的好处。

开始自动化的第一个原因是,你可能不得不再次做这些事情,并且你想确保你每次都以完全相同的方式做它们。你不想忘记一步;你不会想在键盘前等待开始下一步。

不太明显的是,它节省了考虑自动化任务的时间和精力。你可能想要某个特定的结果,甚至知道做 X 会得到 Y,如果跳过 X 很容易,你可能会这样做。就像保持健康一样,你最好的办法是提前做出可执行的决定。自动化不仅可以确保更好的结果,还可以减少实现这些结果所需的精神周期。

自动化开发过程也使得让其他人参与进来变得更加容易。当测试他们的代码和创建拉请求时,你应该简单地希望每个人都遵循相同的步骤吗?你会让他们按照文档中的步骤,像代码猴子一样复制粘贴吗?当然不是!你可以给他们一个脚本,自动执行他们需要遵循的所有步骤,来完成你所做的事情。比这更好的是,如果你建立一个独立的服务,你甚至不需要依靠贡献者自己来运行这些任务。这节省了入职时间、调试时间和压力。

开始自动化

有许多不同的事情你可以开始自动化,包括测试、相关代码检查、发布过程,还有不同的地方你可以开始自动化,例如,本地或使用远程过程。如果没有紧急的和特定的需求,您应该开始自动化(I)会阻碍发布甚至进一步部署的关键任务,如测试,(ii)与开发相关的任务,使其他开发人员更容易参与,以及(iii)与发布相关的后续任务,使生活更容易,并可以减少短期的总线因素。

测试是自动化最简单的,因为您已经有了可以运行的测试。一旦你有了可用和有用的测试,让它们自动运行是很有帮助的,例如,每次你把代码推送到你的存储库的时候。这确保了测试总是运行的,无论您是否记得在本地运行它们。如果你是独立工作的话,这是一点额外的好处,但是这种好处会随着每一个额外的开发人员而增加。您不再需要担心其他人是否在推送他们的代码或提交 pull 请求之前运行了测试,您可以自己看到结果。

持续集成服务

大多数自动化的基础是持续集成服务。这是一项服务——无论是像 Jenkins 这样的自我管理流程还是第三方 tasks 都会为您的项目运行指定的任务。这些通常包括运行测试套件和报告结果,以及部署更新,并且可以运行以响应代码库的更新或由一些其他动作触发。

在这里,我们将介绍一些更受欢迎的第三方服务,以及一个独立应用的基本配置,该应用只需要 Django,并使用一个 runtests.py 文件来启动测试。这些可以作为开始,但更重要的是,它们的共性在高层次上描述了这些服务如何工作。

特拉维斯 CI

Travis CI 是持续集成即服务的鼻祖,至少对于开源软件来说是这样。使用该服务需要一个名为. travis.yml 的配置文件,该文件位于项目的根目录下,并通过 Travis web 应用在 GitHub 上跟踪您的项目。Travis 服务只支持基于 GitHub 的项目。

一个简单的测试设置非常简单:

os: linux
dist: bionic
language: python
python:
  - "3.8"

install:
  - pip install django==3.0
script:
  - python runtests.py

除此之外,Travis 提供了对版本矩阵化的良好支持(类似于 tox),本机支持对多个版本的 Python 并发运行测试。

开源代码库

GitHub 一直是用 Git 托管开源软件项目的最受欢迎的选择。最近,GitHub 增加了“动作”功能,允许在每个项目的基础上运行各种工作流。这些是与单个 YAML 文件一起添加到。github/wor flow/你的项目目录。

实现与 Travis 示例相同的测试运行的示例如下:

name: Blog
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1

      - name: Set up Python 3.8
        uses: actions/setup-python@v1
        with:
          python-version: 3.8

      - name: Install dependencies
        run: |
          pip install Django==3.0

      - name: Run the tests
        run: |
          python runtests.py

这里的 GitHub Actions 测试作业配置比 Travis 配置更详细,这在很大程度上是因为 GitHub Actions 本身并不是一个持续集成,而是一个通用的工作流构建工具,您可以在这个工具上构建自己所需的持续集成任务。这使它更加灵活,但也有点复杂。对大多数人来说,使用 GitHub Actions 的主要好处是,对于已经用 GitHub 托管的项目,它无需任何进一步的集成就可以立即使用。

GitLab

GitLab 是另一种 Git 托管服务,是 GitHub 的替代产品,它将 CI 产品直接集成到 web 应用和可安装的自托管版本中,后者是开源的:

image: "python:3.8"

before_script:
  - pip install django==3.0

stages:
  - Test

test:
  stage: Test
  script:
  - python runtests.py

GitLab 的 CI 服务是专门构建的,因此基本配置很简单。但是,它是高度可定制的,并且与其他 CI 即服务产品不同,它是开源的,可以自我管理。

绕圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈圈

CircleCI 的业务是持续集成,像 Travis 一样,他们为开源项目提供各种免费和付费计划。测试工作流的一个简单示例如下:

version: 2
jobs:
  build:
    docker:
      - image: circleci/python:3.8.0

    working_directory: ~/repo

    steps:
      - checkout

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install django==3.0

      - run:
          name: run tests
          command: |
            . venv/bin/activate
            python runtests.py

其他人

这并不是你可以用来自动化测试你的独立应用的服务或软件的详尽列表。

摘要

在本章中,您已经学习了如何利用自动化系统来简化代码测试,在不同的环境中进行测试,以及为您的应用提供权威的测试 oracle。这些步骤中的每一步都有助于减少原作者和后续贡献者在开发过程中的摩擦。在接下来的章节中,我们将探讨何时在你的应用中使用特定于数据库的功能,以及如何鼓励其他开发者在你的独立应用中进行协作。

二十二、数据库和其他特定于后端的考虑事项

使用像 Django 这样的框架的好处之一是,它为不同的后端服务提供抽象接口,范围从数据库到电子邮件和缓存。一个典型的已部署的 Django 项目只需要支持一个或两个给定的数据库或后端,这有助于在单个项目中使用特定于后端的特性。这些功能可能会提供额外的功能或性能优势,但在发布的独立应用中,应用的使用将仅限于特定的数据库或后端。

在这一章中,我们将简单但独立地回顾关于在你的应用中使用特定数据库选择的问题,包括何时包含特定于后端的特性,以及当你这样做时如何处理它们。

特定于后端的实现和功能

使用像 Django 这样的 ORM 的好处之一是它是数据库不可知的。相同的应用代码可以使用 PostgreSQL、MySQL 甚至 SQLite 运行。然而,有时使用特定于数据库的功能似乎或者确实是有利的。除了共性之外,每个数据库的工作方式不同,提供的功能略有不同,并且有不同的优点和缺点。如果您知道您的项目将使用什么数据库,充分利用这些细节是有意义的。

我们应该注意,这个问题的总体假设是,你的独立应用面向更广泛的受众。如果你的独立应用只是在一个使用通用技术栈的大型组织内部使用,这就没那么重要了。

特定于数据库的代码的最直接的例子是原始 SQL。抛开在已部署的项目中发布原始 SQL 是否是个好主意不谈,使用直接 SQL 的典型好处是,您可以直接依赖数据库特性,包括数据库函数,这些都没有在 ORM 中公开。您通常还可以一次性编写更具表达性的查询。

然而,ORM 是数据库不可知的;甚至 Django 也附带了特定于数据库的特性!contrib.postgres 应用包括 PostgreSQL 特定的数据库函数、RangeField 和 ArrayField 等字段、索引和其他数据库不提供的全文搜索功能(在 Django 中)。

探讨特定于数据库的功能

根据经验,除非你的应用特别关注某种数据库或后端特定的功能,否则除非绝对需要,否则尽量避免依赖这些数据库或后端特定的功能。

依赖特定于后端的功能的一个明显的候选是地理空间功能。GeoDjango,即 contrib.gis,支持多个数据库后端;但是,只有 PostgreSQL 的 PostGIS 完全支持该功能。模型字段可以跨支持的数据库后端使用;然而,不同的地理空间数据库后端对许多查找和功能的支持并不一致。如果实际上使用其中的一个有重要的价值,比如地理空间聚合或边界框重叠,那么这是后端特定(或后端限制)功能的合理使用。

当然,也有解决方法,包括特定于数据库的功能,而不限制开发人员用户的使用范围。一种是包含该功能,但不将其直接集成到其他一些关键功能中,例如,添加一个依赖于特定于后端的功能的查询方法,但不在提供的视图或管理类中使用它。对开发人员用户的另一个好处是,如果他们的数据库后端不支持给定的特性,或者不知道它是否支持(任何定制的后端类都可能使您的代码看不清这一点),那么可以包含警告。

import warnings
from django.conf import settings

if (
        'django.contrib.gis.db.backends.postgis' not in
        [db['ENGINE'] for db in settings.DATABASES.values()]
):
        warnings.warn("""PostGIS not found, not all App features
                        may be supported.""")

一种在某些情况下会起作用的更大胆的方法是完全退出特定于后端的功能。有两种方法可以做到这一点:(I)使用更简单的实现,(ii)允许开发人员用户集成他们自己的类(使用控制反转)。第一种策略将在有限的情况下工作,一个经过验证的地理空间示例是存储地理空间数据,如坐标甚至多边形。如果您的应用没有使用这些数据进行查找或地理空间查询,并且没有提供这样做的清晰用例,那么非地理空间字段就足够了。可以使用 DecimalField 将坐标存储在一对字段中,并使用属性进行访问。

class Place(models.Model):
        latitude = models.DecimalField(...)
        longitude = models.DecimalField(...)

        @property
        def coords(self):
                return self.latitude, self.longitude

        @coords.setter
        def coords(self, lat, lng):
                self.latitude, self.longitude = lat, lng

多边形有点困难,但同样,如果数据只存储或表示在另一层,例如,通过 API 提供给前端应用进行渲染,那么它的存储要求就不那么具体了。ArrayField 可能就足够了(尽管也受 PostgreSQL 的限制),或者 JSONField 也可以(现在 Django 3 中 MySQL 支持)。

需要某种特定于后端的特性的另一种方法是允许某个组件类被注册或用作初始化参数,该初始化参数符合基线接口,但可以完全由最终用户控制。Django 的数据库后端的工作方式符合这一要求,其他项目如 Haystack 的类似功能也符合这一要求。在这两种情况下,您都可以使用 Django 设置向库提供指向后端类或模块的虚线路径,库将加载并使用该特定的类或模块。这意味着后端是无限可定制的(或几乎如此),允许用户在不同的搜索引擎之间切换,也可以创建适合他们特定需求的修改版本,无论这些需求是小功能还是与一个新的完全不同的支持服务合作。

摘要

在这一章中,你已经了解了各种类型的后端特定的选项,以及如何判断使用它们对于你的独立应用来说是否是一个好的策略。

二十三、协作

开源软件倾向于暗示——尽管它并不必须——与其他开发者合作,通常是来自世界各地的陌生人。我们之前的章节很大程度上是基于这样一个想法,即你将与全世界发布和共享你的源代码。这是如此标准的做法,以至于我们很少考虑为什么我们会这样做,以及可能的收益和成本,更不用说如何实现它们了。

协作可能是一个挑战,但它几乎总是值得的。在这一章中,我们将会看到你如何期望人们合作,你作为维护者的角色,以及一些最大化这些贡献的有效性和最小化开源维护负担的策略。

为什么捐款

从回答“为什么”开始是一个好主意,以允许和促进你的项目中的合作,并理解为什么其他人试图以这样或那样的方式为你的项目做出贡献。

作为维护者,贡献的预期或期望的好处包括:

  1. 识别 bug

  2. 更新文档

  3. 建议功能

  4. 开发功能

不管是哪种类型的贡献,大多数贡献背后的共同线索都是希望使用你的软件。用户可能会报告一个错误,因为他们只是想帮忙,让软件变得更好(改善一个富有成效的体验),或者因为他们想修复这个问题,这样他们就可以开始或继续使用软件(修复一个阻塞的错误)。推荐某个功能的人可能希望在你的应用中看到该功能,因为他们正在使用或想要使用你的应用,而该功能会进一步改进它(至少对他们来说!).

创作和维护开源项目的类似原因也可以在 contributing 中找到。这不仅仅是帮助他人的愿望,也是改进他们自己的技术解决方案的愿望,有时也是希望看到他们的建议获得成功。开源软件永远要考虑虚荣心!

我们可以总结为,贡献的三个动机将是纯粹的帮助或做正确事情的愿望(利他主义)、改进产品供自己使用的愿望(实用性)以及有时看到自己的建议付诸实施的愿望(虚荣心)的某种组合。

期待什么

最常见的贡献形式是没有经过充分测试的、记录良好的 pull 请求,这些请求满足了一个提议的特性,甚至解决了一个突出的 bug。最常见的贡献形式是错误报告和特性请求。此外,许多 bug 报告只是简单的问题(有些甚至在文档中有答案)。在某种程度上,这些是对一个项目最简单的“贡献”形式,进入门槛最低。

不管对你的项目有什么贡献,你都需要明白允许和邀请其他人与你合作有好处也有代价。审视和回应问题以及提出要求需要时间,管理他人的期望甚至偶尔的需求也需要情绪能量!有时,即使是善意的人也会忘记或没有意识到这个项目是由其他人管理的,更多的时候是出于他们自己的善意,用他们自己的时间。

因此,无论你能做什么来最小化所有相关方的摩擦,这不仅会提高协作的质量,还会改善你作为维护者的生活。

设定期望

有两种策略可以结合使用,以帮助贡献者增加您的项目,同时最大限度地减少需要您做的额外工作。第一个是自动化(已经在第二十四章讨论过),第二个是非常清楚和坦率地说明合作者应该如何贡献,以及反过来对你有什么期望。

从本地运行的脚本到服务器运行的测试和部署过程,自动化的好处是多方面的。一个显著的好处是,当测试和检查由持续集成服务运行时,您不需要自己在本地检查和运行测试来验证新代码。还有一个更具社会性的观点是,为某些决策提供自动化的预言减轻了你的决策负担,也减少了当贡献者可能不同意某个决策或因某个决策而感到威胁时的指责焦点。一些简单的例子包括代码格式和覆盖率。如果你已经对这些东西进行了自动检查,即使你已经设置了它们,如果有人的贡献使它们失败了,你不需要说“我不接受这个,因为我做了或看了 X,它不够好”,你可以简单地说“啊,一旦你让东西通过那里,我们可以把它合并进来。”自动化中的规则不太可能让人生气,你需要做的决定和沟通的也更少。

协作过程中总会有一些重要的方面无法自动化,同样,这些可以通过清晰地记录期望来解决或改进。这不仅会让你的生活更轻松,也会让贡献者更轻松,通常会提高他们贡献的质量。

首先是投稿指南,通常包含在顶级独立文件中,如 CONTRIBUTING.rst。这是一个独立文件还是包含在您的自述文件中并不重要,重要的是它包含了什么。在这里,您有机会向潜在用户表达最佳的沟通渠道是什么,他们应该在哪里报告 bug,报告 bug 时应该包括哪些信息,甚至在他们报告 bug 后,对您或其他维护人员有什么期望。

根据您使用的代码发布服务(例如 GitHub、GitLab),您可以创建模板,协作用户可以根据这些模板生成报告或请求。模板的好处是,您可以提前询问评估问题所需的信息。基于 GitHub 的项目的一个简单问题模板可能如下所示, 1 它会提示报告者所需的调试信息:

### Bug reports:

Please replace this line with a brief summary of your issue **AND** the  following information:

- [ ] 1\. Python version:
- [ ] 2\. Django version:
- [ ] 3\. MyApp version:
- [ ] 4\. Reproducible test code (if feasible/relevant):

### Features:

**Please note by far the quickest way to get a new feature is to file a Pull Request.**

We will consider your request, but it may be closed if it's something we're not actively planning to work on.

同样类型的模板也可以被复制用于拉取请求 2 :

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue).
- [ ] New feature (non-breaking change which adds functionality).
- [ ] Breaking change (fix or feature that would cause existing functionality to change).
- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.

这允许提交者检查他们所代表的已经完成的内容,明确并提前说明期望的内容,使您的审查更容易,并且还将决策和可能的更新请求委托给策略。然而,如果您浏览 pull request 模板,您会注意到至少有几项可以进一步委托给自动化脚本,例如,验证所有测试是否通过,以及在合理的程度上,代码是否匹配期望的项目风格。

你可以考虑的另一个帮助文档的特性是行为准则。简而言之,行为准则是一个期望文档,预先声明项目中的协作人员应该如何一起工作和交流。这些变得越来越流行,主要是为了应对开源协作中可能出现的有害或敌对行为。然而,即使你并不十分担心“巨魔”,行为准则也是可以授权的,而不是可以感知的临时决定。

维护开放源代码的角色和义务

维护者的角色是什么?你应该如何定义你对用户和贡献者的义务?这些是故意刺激的问题,分享了一些关于维护者“工作”的流行假设。简而言之,这两个问题最终都要由你来决定。

维护人员的角色是回答问题和指导开发,这在很大程度上取决于项目开发的活跃程度,包括涉及到多少其他贡献者。如果有积极的贡献者和发展,你可以把你的角色很大程度上变成一个交通警察,给人们指出正确的方向,阻止他们发生事故。或者你可能是所有后续更新的主要作者。

义务的问题更加棘手。这对你所使用的流程没有太大的直接影响,只是对你自己的健康更重要。当你选择免费发布开源软件,人们就会使用它。随后,即使是善意的用户和贡献者也可能会向项目维护人员提出他们认为您有义务满足的请求或要求。这可以从发布新版本到添加新的所需功能。对于采用率高的大项目来说,这可能是压倒性的,对于维护人员来说,即使是在错误的时间的小项目也可能会受到困扰。

回答你的义务这个问题的关键是对我们共享使用的东西负责和你的用户和贡献者使用免费(如“免费啤酒”)软件这一事实之间的矛盾。每一个附属问题都取决于后一个事实,正因为如此,你发布新版本或开发新功能的任何义务都完全是你自己的构建和你免除的自由。

这最终意味着对你发布的 Django 应用的状态和已知问题的状态都要保持坦率和透明。这可能是因为你的应用还没有准备好投入生产,或者某个特定的 bug 需要比你现在更多的时间来解决。你没有义务按照其他人的时间表准备好你的应用,或者修复错误——真的。你甚至没有义务告诉任何人你什么时候会这样做,尽管真诚地努力沟通有助于在当前和潜在的贡献者中建立良好的意愿。

最终,我们的免费开源软件是免费提供的。代码可以被评估,可以被修改以供个人使用,除了明确要求用户支付某种费用的交易,古训买者自负“让买家当心”是核心义务。

摘要

在这一章中,你已经了解了潜在贡献者希望在你的项目中合作的方式,以及一些鼓励高价值和有效合作的策略。这包括交流如何做出不同种类的贡献,以及如何设想你与项目和用户的关系,以避免感到不知所措。

在下一章,我们将简要介绍如何使用应用模板来创建 Django 独立应用。

Footnotes 1

基于来自 github 的公共领域问题。com/Steve Mao/github-ISSUE-templates/blob/master/check list/ISSUE _ TEMPLATE。md

  2

同样来自 GitHub 问题模板库

 

二十四、使用应用模板

一旦你掌握了创建自己的独立应用的诀窍,你可能会想开始写更多。在这一点上,你可能会发现有很多你不想一遍又一遍地做的决定,还有一些你不想写的必要的样板文件。解决这个问题的一个办法是从模板开始创建应用。

在这一章中,我们将回顾一些从模板创建新的独立应用的选项,包括 Django 自己的 startapp 管理命令和古老的 Cookiecutter 工具。

启动应用

您可能已经了解并使用 Django 的 startapp 管理命令在您自己的 Django 项目中创建新的应用:

./manage.py startapp myapp

默认情况下,该命令将采用一个应用名称,并基于 Django 包中的模板目录结构创建一个具有最小文件结构的目录,包括 models.py 和 tests.py 文件:

myapp/
        migrations/
        __init__.py
        admin.py
        app.py
        models.py
        tests.py
        views.py

该命令的核心功能是在现有项目的环境中创建应用。然而,该命令没有理由不能用于在其他任何地方创建应用结构;只需使用 django-admin 脚本:

django-admin startapp myapp

这本身并不十分有用,因为唯一的好处是在一个特定的目录中创建了一个除空白文件之外的小集合。这可以通过创建和使用您自己的模板目录并使用- template 标志向 startapp 命令提供一个参数来改进,例如:

django-admin.py startapp myapp --template ~/app.template

通过使用您自己的模板,您不仅可以选择使用不同的文件,还可以用常用的导入和您使用的其他代码预先填充它们。startapp 命令支持一些特定的上下文变量,包括应用名称,因此您还可以在这些文件中包含一些特定于应用的引用。此外,您可以更改整个结构,包括将应用作为一个包放在父目录中,其中包含您的设置文件、自述文件等。

如果你每次都基于完全相同的结构和特性集来创建应用,这种策略可能就足够了。然而,最低限度支持的模板上下文意味着即使有模板支持,也没有多少可配置的空间。

饼干成型切割刀

对于更健壮的替代方法,可以考虑使用 Cookiecutter。Cookiecutter 是一个 Python 包,被描述为“一个从 cookiecutter(项目模板)创建项目的命令行实用程序”。使用 Jinja 模板和 cookiecutter JSON 配置文件的组合,您可以在交互式提示符下基于单个 cookiecutter 创建高度可配置的项目。

在使用 pip、brew(在 macOS 上)或 apt-get(在 Debian 上)进行安装之后,从远程模板创建项目是一个简单的命令:

  • 值得注意的是,虽然 cookiecutter 是一个 Python 包,并在 Python 社区中广泛使用,但 Cookiecutter 项目模板可以被创建并用于任何类型的项目,与语言无关。
cookiecutter https://location.com/of-the-cookiecutter.git

也许比 cookiecutter 提供的特性集更重要的是公开共享的 Cookiecutter 项目模板社区,包括用于创建“普通”Python 包和 Django 独立应用的模板。使用社区构建的模板的好处是多方面的,包括消除从零开始创建模板所需的时间和各种决策,以及“免费”获得大量已经过众多用户审核的最佳实践。

主要缺点是,模板可能会排除一些您想要的特性,但更多的是,它们可能过于复杂且特性丰富,无法满足您的需求。编辑可能比从头开始更费力。在最流行的 Django 包 cookiecutter,py Danny/cookiecutter-Django package 中,没有包含太多过于具体的决定,这意味着它是一个安全的起点,不会添加不必要的 cruft。它将执行一个顶级包(而不是使用源目录),并且特定的 Python 和 Django 版本可能不是最新的。谢天谢地,你可以改变这些事情。图 24-1 提供了 py Danny/cookiecutter-Django package 为配置新的 Django 独立应用提供的提示示例。

img/486718_1_En_24_Fig1_HTML.jpg

图 24-1

py Danny/cookiecutter-django package 提供的提示

有两种方法可以创建您自己的 cookiecutter 项目模板来启动 Django 独立应用:从头开始或者修改现有的项目模板。修改现有的 cookiecutter 非常简单,只需克隆源代码库,进行必要的修改,并使用您的本地克隆作为模板。

cookiecutter path/to/local/cookiecutter

改编一个现有的模板意味着你不必从头开始一切,从计算文件的模板名称到决定如何跟踪各种包的依赖关系。如果你需要的改变太激进,你可以从头开始。从头开始时,请记住,尽管模板有巨大的价值,但模板的核心是将文件从一个源复制到另一个源。换句话说,用尽可能少的配置创建您的结构,只在您需要的时候开始并构建可配置性。

摘要

在这一章中,我们看了从模板创建新的 Django 独立应用的两种方法:使用 Django 的 startapp 命令和通用项目模板工具 Cookiecutter。两者都可以与定制的起始模板一起使用,以加强后续 Django 独立应用的项目设计决策;然而,Cookiecutter 的模板比 startapp 更灵活,应该作为独立的应用模板解决方案优先考虑。

第一部分:Django 应用的基本组件

第二部分:复制和提取可重用的应用

第四部分:管理您的独立应用