Django 独立应用教程(全)
一、定义 Django 独立应用的范围
每个软件项目都是由边界定义的,不管你是否有意选择了它们。在这一章中,我们将通过探索开发——和共享——你的独立应用的好处,开始我们的 Django 独立应用的冒险,以及如何考虑带来什么其他依赖,甚至你的 Django 应用是否应该是一个 Django 应用。
创建独立应用的优势
创建一个 Django 独立应用的好处是多种多样的,有利他的,利己的,也有纯粹实用的。
分享您的作品
假设你计划将你的应用作为开源包发布,大多数人想到的第一个好处是与他人分享你的工作的价值。在软件社区中,共享代码已经有很长的历史了(只要还有囤积的历史!)因为在大多数情况下,这是一种其他人可以从你已经做的事情中受益,而不会让你失去任何机会的方式。除非你认为你能够授权和销售你的 Django 应用,否则它本身不太可能是一个盈利的企业。既然你已经从别人免费分享的作品中获益,为什么不分享回来呢?
提高代码质量
一旦你开始分享你的工作,一个连锁反应往往是你的应用本身质量的提高。大多数人第一次公开分享他们的作品时会感到有点紧张,因为现在每个人都可以仔细检查。但是知道其他人会阅读和/或使用你的代码会让你更加小心你如何设计和开发它。一旦有足够多的人使用它,你将开始吸引其他人的贡献,无论是以错误报告的形式(是的,识别错误是一种贡献!)、错误修复、功能建议和新功能。
不要重复你自己
如果你正在寻找更实际的原因,是时候开始思考应用“可重用”的含义了作为开发人员,拥有可重用的代码可以节省时间和降低风险。您可以使用已经创建的内容和您知道已经有效的内容。
想象一下,如果每次创建一个新的 Django 项目,您都必须从头开始编写自己的认证系统,包括视图、中间件和模型。不可避免的是,您可能只是将代码从一个项目复制并粘贴到另一个项目,希望留下错误,并且从来没有真正的最佳实践集合。谢天谢地,你不必这么做,因为 Django 的 contrib 模块中就有这样一个可重用的应用。
这就谈到了风险的问题。代码中的风险可以从 bug 到你没有想到的重要特性,并且,所有的事情都是平等的,当你从零开始独自工作时,这些风险会增加。我们从可重用应用中寻求的不仅仅是从从头开始编写代码中节省的时间,还有最佳实践的积累,来自真实世界使用的功能,以及有机会被消除的错误。
整个公司的通用性
对于支持各种已部署的 Django 应用的开发人员和团队来说,支持一个公司的一个产品或多个产品,例如,将通用功能提取到可重用的应用中,这是一种确保功能在任何地方都能按预期工作的方式,并使您能够修复 bug 或添加关键的新功能,以便可以在任何使用它的地方部署版本更新。
示例包括定制的用户应用、特定领域的模型(例如,健康科学、金融)、资产管理和通用工具。
客户项目的共性
对于自由职业者或代理角色的开发人员和团队来说,与各种客户和项目一起工作,可重用的应用是跨项目携带有用功能的一种方式。
Django 中大多数流行的内容管理和电子商务解决方案都是这样产生的。Django CMS 和 Wagtail 是两个流行的基于 Django 的内容管理系统,也是独立的 Django 应用,由创意机构开发,作为客户项目的解决方案。
声望的流通
最后,虽然通常不言而喻,但有一个受欢迎的或至少有用的独立应用,这是一种可感知的声望——我们不会用“虚荣”这个词。对于产品团队、代理团队和自由职业者来说,一个共享的独立应用是能力、观点和视野的展示。它可以用来吸引潜在的雇员和潜在的客户。
前面提到的内容管理系统 Django CMS 和 Wagtail 分别由 Divio 和 Torchbox 创建。他们做了伟大的工作,但是有多少 Django 开发者会知道他们的名字不是为了他们各自 CMS 的贡献?他们在招聘中的知名度现在肯定有所提高。我自己不能说出任何特别的恶名,但是由于我自己发布的 Django 应用,一些客户直接或间接地发现了我。
现在,如果这是你想创建和发布 Django 独立应用的唯一原因,我建议你把注意力转移到别的地方。拥有一个被大量人使用的应用的连锁效应可能会慢慢积累,你也不太可能喜欢构建和维护应用的过程,你的用户群也可能会受到影响。
但是,如果你的目标包括与社区共享、创建不断改进的代码库、减少开发时间和风险,那么不管你的 GitHub 档案为你赢得多少虚假的互联网积分,努力都是值得的。
有没有 Django?
在定义一个潜在的 Django 独立应用的范围时,你需要问的第一个问题是,你心目中的包是否需要是一个 Django 应用,或者仅仅是一个使用 Django 的 Python 包,第二个问题是,即使它不是一个应用,你的包也应该是特定于 Django 的。
第一个问题是一个有点小的问题,但是涉及到你的应用应该如何集成到其他 Django 项目和开发工作流中。很容易在 Python 文件中看到 Django 导入,并认为“啊,这是一个应用”,而实际上它是不可安装的库代码(不可安装是指 INSTALLED_APPS,而不是指已安装的包)。例如,如果您想要分发一些额外的表单字段,这些字段可以作为“常规”Python 库分发和使用,而无需将模块添加到 INSTALLED_APPS 列表中。你仍然需要考虑后续章节中讨论的问题,比如测试(3,21),文档(22),和打包(8),但是在你如何设置你的测试和文档使用上有一些小的不同。
另一方面,您预期的包在 Django 项目中可能非常有用,但对 Django 没有硬性要求,不知道 web 框架甚至根本不知道用于 web 应用。如果是这样,那么你应该沿着这条路走下去。
为什么要保证这种分离?首先,如果你要与世界其他地方分享,并且核心功能实际上不依赖于 Django,那么你已经扩大了受众。您还减少了您的包中的另一个依赖项,即使您在 Django 项目中使用它,这也是另一个可能中断的依赖项。在有意义的地方,参考标准库而不是 Django 实用程序。如果有东西在新的 Django 版本中移动,你现在就与这种变化绝缘了。
值得记住的是,您可以向 Django 项目添加功能,而无需特定于 Django 的模块,或者不一定需要 Django。在关于混合依赖支持的第十六章中,我们将研究如何区分哪些是 Django 特有的,哪些不是。
选择依赖关系
像您正在创建的这样的包的伟大之处在于,它们为您提供了免费的功能——也许不是免费的,但是不需要您自己编写代码和找出边缘情况的成本。这些好处假设您正在使用的依赖项已经过测试,并且如宣传的那样工作,并且它们继续受到新版本的 Python、Django 及其各自的依赖项的支持。
您添加的每个依赖项都增加了您需要测试的表面积,以及中断交互的机会。在 Django 项目中是这样,在你自己的独立应用中也是这样。现在,自己重写一切当然是不明智的!但是要仔细考虑是否真的需要在项目中添加每个依赖项。
在引导性问题中,您应该考虑
-
依赖关系是否为您的应用提供了必要的功能?
-
你将支持的 Django 版本是最新的吗?
-
它有什么样的测试和文档覆盖?
-
维护者看起来有多忠诚?
必要的功能
向项目和库添加依赖项是很容易的。在大多数 Django 项目中,项目只有一个“消费者”,即项目团队。这可能是一个拥有多个不同环境部署和数百万用户的大型团队,但该团队和该应用仍然是唯一的消费者。有了像 Django 独立应用这样的可重用库,您可以预期它将被用于数十个,甚至数千个其他项目。应用的每个依赖项都是使用应用的项目的依赖项。如果你添加到应用中的每个依赖项包含错误或不兼容的代码,并使你面临风险,那么它就是一个潜在的拦截器。
这与您在 Django 项目中添加依赖项的方式没有太大的不同;然而,当您控制最终部署并且其他人不再依赖您自己的代码时,退出这些更改会更容易。
所以问一下是否有必要增加这一点,不是从严格的逻辑意义上,而是是否增加了更多的便利。
这是我犯的一个“错误”。我自己案例中的一个例子是,我决定使用 django-extensions 应用作为我自己的 django-organizations 的依赖项。我想要一个带时间戳的模型——这是一个好东西,当它丢失时,你会注意到它——此外,我还想要一个会自己处理的 slug 字段。为此,我想要 AutoslugField。与其说这是一个限制性的决定,不如说是一个糟糕的决定。然而,我可以使用一个典型的 Django slug 字段,但是出于我自己的需要,Autoslug 就在那里。后来我意识到,有些人,包括我自己,可能需要能够配置 slugs 是如何制造的,这不应该如此固定。
版本兼容性
由于 Python 2 的寿终正寝,与两年前甚至一年前相比,现在对 Python 版本兼容性的考虑已经不那么重要了。然而,Django 兼容性仍然是一个问题。正在讨论的依赖项是否支持您经常使用的 Django 的当前版本?这是一个明显的开始问题。
但是你也想知道它是否支持旧版本,是否有努力支持未来的版本。重要的是,您添加的任何依赖项都支持您的应用将支持的 Python 和 Django 的相同版本。
一个很好的经验法则是寻找对 Django 长期支持(LTS)版本的支持。
测试和记录
当在依赖关系中寻找测试和文档时,我们寻找几样东西:
-
验证代码是否有效,是否可以测试 bug
-
解释如何安装和使用软件包,以及它存在的原因
-
表示有人关心
当然,您应该在依赖项中找到测试,以及广泛覆盖软件包功能的测试。您还应该使用持续集成(CI)系统来寻找自动化测试运行,这样您就可以看到当有人将代码推送到存储库时,测试总是在运行。很难相信有人只是在自己运行测试,如果没有与存储库相关的 CI 系统,那么您将不得不相信贡献者正在运行测试,并且他们通过了测试。
基本文档对于确保您知道如何设置依赖关系以及可能的边缘情况非常重要。例如,它做了什么意想不到的事情或者它与什么相关的包不兼容?对于较大的依赖关系,大量的文档是必须的,但是对于较小的依赖关系,文档不仅是一个帮助,也是一个信号,表明这个包有一个特定的使命(用例),并且提供了足够的上下文,您可以判断作者和/或维护者是否已经考虑和支持了超出他们自己的初始问题的各种用例和开发人员。
第三点直接导致维护节奏。
维护节奏
这种依赖看起来会得到积极的支持吗?是否有大量未解决的问题,特别是 bug,没有被解决?是否存在未完成的旧的拉请求,尤其是针对 bug 的拉请求?许多人忽略的一个更重要的问题是,即使报告了问题并合并了拉请求,这些会导致新的发布吗?如果在过去的一年中合并了拉式请求,但是在此期间没有发布新的版本,那么它可能没有得到充分的维护。
特定与一般
作为程序员,我们倾向于构建一个解决方案,然后看看如何将这个解决方案抽象出来以解决更多的问题。可以说,这是一件非常好的事情。然而,我们很容易忘乎所以,试图覆盖太多的领域,陷入一个漂亮的抽象的兔子洞,而不是“仅仅”解决你面前的具体问题。
例如,您可能有一个用 Stripe 构建的 SaaS 应用订阅管理库。一个更一般化的方法将考虑不同的订阅和支付后端。然而,除非你自己积极地利用这些不同的场景,否则试图处理它们很可能导致半途而废的解决方案。当你开始使用应用时,创建一个能够处理用户定制场景的更通用的系统将会使你花费更多的时间在抽象上,这些时间可以更好地用于完善你的应用。
编写你的应用来涵盖你能想到的每一个用例几乎肯定是一个错误,不管更一般的用例有多有用。你很可能花时间预测非问题,却没有预测到实际需求。比起构建一个解决不了任何明确问题的过于抽象的解决方案,添加更多的用例,或者制定更通用的、可行的解决方案要好得多。
摘要
在本章中,您了解了创建独立应用的好处,包括分享常见问题的解决方案的机会、通过开源审查帮助提高代码质量,以及通过标准化常见问题来改进开发过程。您还了解了 Django 相关的 Python 项目和 Django 应用之间的区别,以及如何权衡附加依赖项的包含。在下一章,我们将研究一个独立应用的结构。
二、构建独立的 Django 应用
除了包含在您的独立 Django 应用中的功能之外,在更实际的层面上,您还需要构建代码以供重用。一个独立的 Django 应用的结构和一个直接嵌入到你的 Django 项目代码库中的 Django 应用的结构基本上没有什么不同。然而,有几个你想要遵循的实践来最大化你的应用的可用性。我们将在本章中讨论这些问题。
作为 Python 模块的 Django 应用
让我们重申,Django 应用,不管是独立的还是其他的,都是一个 Python 包。也就是说,它由多个 Python 模块(即 Python 文件)组成,可以从其他包导入,也可以从其他包导入。让 Python 包成为 Django 应用的具体原因是,它具有只能在 Django 项目中使用的功能、类和函数,方法是将它们显式包含在项目的 INSTALLED_APPS 列表中。包存在于 Python 路径中是不够的。
你可以把任何你想添加的 Python 包添加到 INSTALLED_APPS 中,但是如果它不是 Django 应用,它绝对不会为你做任何事情。我们可以把 Django 应用想象成一个接口,或者我们有一些抽象的东西,比如基类(Python 的 abc 模块),但是对于模块,它可能看起来像这样:
-
该包包括具有一个或多个具体模型的模型包。
-
该包包括一个模板标签模块,其中包含标签模块库。
-
这个包包括一个 HTML 模板的模板目录。
-
这个包包含一个静态资产目录,其中包含图像、CSS 文件、JavaScript 文件等等。
-
该包包括管理命令,即应用(myapp)的 myapp.management.commands 模块中的模块,定义了从 django . core . management . base Command 继承的命令类
-
该包定义了一个默认的 app.config 类
前五个中的任何一个都足以提供需要安装 Django 应用的功能。最后,定义一个默认的 AppConfig 类,这是一个最佳实践,但是就其本身而言,它并没有提供太多的功能。它确实允许您对基本的应用配置和名称空间进行更改(稍后将详细介绍)。
匹配预期的接口就足够了,可以从你的 Django 应用提供可安装的内容,不管是独立的还是其他的。知道了这一点,即使在你自己的项目中运行良好,也要避免单独依赖你的独立应用的模块接口。创建独立应用的目的是允许在所有 Django 项目中重用,所以你应该尽可能的清晰。
从历史上看,确保一个包被识别为 Django 应用的唯一步骤是包含一个模型模块。如果你在假设这种情况仍然存在的情况下工作,那它就不是。除非您在应用中包含模型类,否则没有必要包含模型模块。如果你的应用只包含模板标签,你可以只包含以下内容,包括用于确保目录是一个包的__init__.py
文件,用于包含任何和所有模板标签库的templatetags
模块,以及用于定义标签库的boo_tags.py
文件,该标签库包含模板标签和/或可以使用{% load boo_tags %}
加载到模板中的过滤器:
boo
|── __init__.py
|── templatetags
│ |── __init__.py
│ |── boo_tags.py
现在,如果您想使用 boo_tags 中的模板标记 boo,您需要做的就是将 boo 添加到 INSTALLED_APPS 中,并且您可以在项目中的任何位置加载标记库。
- 包含自定义标签的应用必须位于 INSTALLED_APPS 中,以便{% load %}标签能够正常工作。这是一个安全特性:它允许您在一台主机上托管许多模板库的 Python 代码,而无需为每个 Django 安装启用对所有模板库的访问。 1
中间件、URL 和视图呢?
许多 Django 应用包含了额外的 Django 相关特性,比如中间件、视图、URL 定义和上下文处理器。它甚至在 Django 文档中如此正确地建议:
- 应用包括模型、视图、模板、模板标签、静态文件、URL、中间件等的某种组合。它们通常通过 INSTALLED_APPS 设置和其他机制(如 URLconfs、中间件设置或模板继承)连接到项目中。
这些功能对一些 Django 应用是有益的,甚至是必要的,但严格来说,它们不需要 Django 应用来使用。您可以包含任何 Python 包中的 URL、中间件类、表单,甚至视图,无论它是 INSTALLED_APPS 中的 Django 应用,还是路径上可用的 Python 包。
Django 库并没有因为它不是一个可安装的应用而变得不那么有用。例如,表单、中间件和视图是 Django 项目的核心组件。您的库如何集成到其他 Django 项目中会略有不同,但是计划、测试、开发和维护您的库的步骤不会有很大的不同。
示例应用:货币
我们将从一个非常基本的示例应用开始。这是一个让处理货币更容易的应用。在它们的基础上,货币值只是数值,特别是十进制值,指的是特定面额的金额,通常在特定的时间点。大约 10 美元不同于€10 美元,2015 年的 10 美元也不同于 1990 年的 10 美元。
我们想做的是让切换货币金额的显示更容易,并轻松地格式化它们。首先,我们只是想改变某些数字的格式,所以我们只是添加了几个模板过滤器。
摆在我们面前的问题是这样的:它一定是 Django app 吗?随着我们的构建,越来越多的功能可能是非 Django 特有的,但是如果我们要添加模板标签,它们必须是 Django 应用的一部分。否则,我们无法加载标签库。由于这包括一个必须从 INSTALLED_APPS 中已安装的应用访问的功能,这将是一个 Django 应用。
我们将启动名为 currency 的应用,首先只需要一些必要的文件。文件结构将如下所示:
currency
|── __init__.py
|── apps.py
|── templatetags
│ |── __init__.py
│ |── currency_tags.py
|── tests.py
包含一个__init__.py
文件的 currency 文件夹定义了我们的模块。我们的核心功能现在只是模板标签和过滤器,所以我们只是有一个模板标签模块,再加上__
init__
.py
文件,然后是标签库名称。
我们的测试有一个 tests.py 文件,然后是一个 apps.py 文件。为了满足 Django 应用的要求,我们的包必须定义一个 models.py 文件或一个 apps.py 文件,最好是包含后者,甚至包含 models.py 文件。
所以现在我们来看内容。我们的 init。py 文件是空的(暂时)。
这是我们的 apps.py 文件:
from django.apps import AppConfig
class CurrencyConfig(AppConfig):
name = "currency"
verbose_name = "Currency"
这是我们在currency_tags.py
中的标签库:
from django import template
register = template.Library()
@register.filter
def accounting(value):
return "({0})".format(value) if value < 0 else "{0}".format(value)
这是我们的 tests.py 文件:
import unittest
from currency.templatetags.currency_tags import accounting
class TestTemplateFilters(unittest.TestCase):
def test_positive_value(self):
self.assertEqual("10", accounting(10))
def test_zero_value(self):
self.assertEqual("0", accounting(0))
def test_negative_value(self):
self.assertEqual("(10)", accounting(-10))
摘要
在这一章中,你学习了 Django 应用的组成和结构,以及如何区分包含对 Django 项目有用的功能的 Python 包和必须是 Django 应用的 Python 包。在下一章,我们将看看你的 Django 应用的测试,包括它们的价值和如何包含它们。
三、测试
测试确保我们的代码做我们期望它做的事情。他们还确保对代码库的更改不会破坏一些意想不到的东西,并且他们向我们应用的其他用户发出信号,他们可以依赖代码。
在这一章中,你将确切地了解测试如何在一个独立的 Django 应用中提供价值,如何在 Django 项目之外运行你的应用的测试,如何测试更复杂的多应用关系,如何包含你的测试,以及你是否需要配置 Django 来测试你的应用。
为什么要测试?
每个人都说你应该测试。这听起来很明显——如果测试是好的,我们应该去做。但是这回避了关于测试的好处的问题。
测试你的 Django 应用有几个目的。测试与应用代码一起编写,或者在应用代码之前编写,有助于提供一个可以用来验证代码的工作规范。在这种情况下,他们还可以帮助塑造代码和界面,就像你从零开始添加一些功能一样,测试会给你第一次使用它的机会。
一旦就位,即使是无关紧要的测试也可以防止代码库看似无关紧要的变化所带来的倒退。
虽然不是它们的主要用途,测试也可以提供一个如何使用你的代码的例子。在这种情况下,它们当然不能代替适当的文档,但是作为代码示例的测试——特别是当测试自动运行时——是一种可以验证是否是最新的文档形式。
所有这一切的基础是这样一个事实,计算机程序是由人类编写的,而我们人类在自己编写可靠的代码时是非常不可靠的(如果这不适用于你,请原谅)。有各种各样我们无法预测的事情,我们不擅长马上看到的边缘情况,以及在代码表面不明显的交互。
测试并不能解决所有这些问题,但是测试提供了一个有效的工具来消除我们代码的不确定性。最终,测试为你和你的应用的其他用户提供了信心——不要忘记“未来的你”很可能是这些用户中的一员!
测试来自 Django 项目的应用
Django 项目提供了一种使用测试管理命令运行测试的方法:
python manage.py test
这个命令将运行 Django 项目中的所有测试。通过结合使用测试管理命令和应用名称,可以将范围缩小到只运行单独命名的应用,如下所示:
python manage.py test myapp
如果非常简单的 myapp 看起来像这样
myapp/
__init__.py
models.py
tests.py
使用一个简单的 tests.py 文件,如下所示
from django.test import TestCase
from myapp.models import SomeModel
class TestSomeModel(TestCase):
def test_str_method(self):
instance = SomeModel()
self.assertEqual(f"{instance}", "<Unnamed Instance>")
然后,命令 python manage.py test myapp 将使用 Django 的默认测试运行程序运行 myapp.tests 中的所有测试,例如,对于给定的示例测试文件,该命令将运行 TestSomeModel.test_str_method。
例如,如果你在一个工作项目的上下文中开发你的应用,当你在一个更大的 Django 项目中工作时,这就很好了。如果你的应用是一个独立的库,其中的代码是从项目的之外的管理的,那么它的帮助就小得多。对于一个独立的应用来说,能够像任何其他 Python 包一样运行测试会更好。
测试应用
如果您以前使用过其他 Python 包,您会注意到它们是以一种简单的方式进行测试的。在某个地方会有一个测试模块,通常 setup.py 文件会定义一个测试脚本来使用 python setup.py 测试命令运行。这也适用于使用 Django 的包,但需要注意的是,Django 的许多功能必须在 Django 项目的上下文中运行,这是 Python 的 unittest 不会为您处理的。
为了激发一些测试独立应用的合理方法,让我们考虑一下测试应用最直接可用的策略:从您正在使用应用的任何项目中进行测试(假设您正在提取它)。
这意味着,要测试 myapp 应用,它需要安装在与您的工作项目相同的路径上,即相同的虚拟环境中,并且它需要位于您的工作项目的 INSTALLED_APPS 中。当需要测试对 myapp 的更改时,您需要回到工作项目来运行它们,也就是说,运行。/manage.py 测试 myapp。
如果这听起来不太合理,那么你就在正确的轨道上。然而,这种策略不允许测试一个独立的应用,这意味着它对于不参与您的项目的任何人来说都是不可重复的。如果你打算打包应用以供重用,你将无法求助于你的原始项目。谢天谢地,有更好的方法。
项目之外的测试
为了激励我们的后续解决方案,我们将尽可能设置最明显的解决方案。这将需要建立一个虚拟的,或持有人,项目,并从那里运行测试。为此,我们将在应用的根文件夹中创建新的 Django 项目,与应用源文件夹本身并行。该项目将把我们的应用包含在 INSTALLED_APPS 列表中。
然后,运行测试和任何其他命令就像调用 holder 项目的 manage.py 文件一样简单,就像任何其他项目一样。
下一步是在包根中创建一个示例项目,这将是一个只包含我们的应用的精简项目。现在,我们可以直接在包中运行 manage.py 命令并测试应用。您甚至可以在项目根目录下添加一个 bash 脚本,无论测试位于何处,它都会执行测试。
#!/bin/bash
cd sample_project
python manage.py test myapp
这是布局的样子:
sample_project
__init__.py
settings.py
url.spy
wsgi.py
__init__.py
manage.py
myapp/
__init__.py
models.py
tests.py
然后,为了运行应用的测试,您可以从示例项目中运行它们,就像它是一个生产就绪的 Django 项目一样:
python manage.py test myapp
这是可行的,并且是对原始示例的改进,但是它仍然增加了运行我们的测试所必需的内容。
使用测试脚本
当然,Django 不要求我们有项目支架,只要求配置 Django 设置。因此,更好的解决方案是使用 Python 脚本来配置这些最低限度的设置,然后运行测试。
该脚本需要做三件事:
-
定义或配置 Django 设置
-
触发 Django 初始化(即使用 django.setup())
-
执行测试运行程序
有两种方法可以提供 Django 设置。一种是用 settings.configure()的关键字参数在测试脚本中直接配置它们。另一种方法是指向一个仅供测试的 settings.py 模块,就像您运行生产应用一样。以下是前者的一个小例子:
#!/usr/bin/env python
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
settings.configure(
DATABASES={"default": {
"ENGINE": "django.db.backends.sqlite3"
}},
ROOT_URLCONF="tests.urls",
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"myapp",
],
) # Minimal Django settings required for our tests
django.setup() # configures Django
TestRunner = get_runner(settings) # Gets the test runner class
test_runner = TestRunner() # Creates an instance of the test runner
failures = test_runner.run_tests(["tests"]) # Run tests and gather failures
sys.exit(bool(failures)) # Exits script with error code 1 if any failures
而是使用一个设置模块(来自下面的 Django 文档)。这在功能上与前面的代码相同,只是它将设置分解到一个更典型的设置文件中,在本例中为tests/test_settings.py
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
为什么要选择一个而不是另一个?如果您对设置有其他需求,使用单独的设置模块将更加灵活。脚本内配置风格可以满足更简单的应用,例如,没有模型的应用。
在第二十二章中,我们将研究一种更符合人体工程学的方法来管理你的测试和测试配置。
测试应用关系
但是,当你的 Django 应用被设计成与其他应用一起使用,或者与它们一起使用时,会发生什么呢?仅仅孤立地测试你的应用是不够的。在这种情况下,您需要创建示例应用,并将它们包含在您的测试设置中。
假设您的应用提供了基本模型。对于我们的例子来说,这是一个非常基本的电子商务模块,让人们可以根据他们想要的任何模型制作产品,并添加一些基本字段,如价格、SKU 以及是否活跃销售。该应用还包括一个 queryset 类,其中定义了一些有用的方法。由于我们的模型类是抽象的,queryset 类必须与用户应用中的具体模型相关联。
class ProductsQuerySet(models.QuerySet):
def in_stock(self):
return self.filter(is_in_stock=True)
class ProductBase(models.Model):
sku = models.CharField()
price = models.DecimalField()
is_in_stock = models.BooleanField()
class Meta:
abstract = True
现在,为了测试这一点,我们需要一个具体的模型(无论如何,实际使用基本模型进行测试是有帮助的)。为此,我们需要另一个应用,它定义了一个从抽象模型继承的具体模型,并使用提供的 queryset。
这样的应用只需要提供成为应用的最低要求,特别是 models.py 模块:
test_app/
migrations/ ...
__init__.py
models.py
在模型文件中,使用应用的抽象基础模型定义一个模型:
from myapp.models import ProductBase, ProductQuerySet
class Pen(ProductBase):
"""Testing app model"""
name = models.CharField()
pen_type = models.CharField()
objects = ProductQuerySet.as_manager()
定义模型后,确保测试应用包含在您的测试设置 INSTALLED_APPS 中:
INSTALLED_APPS = [
'myapp',
'test_app',
]
注意,如果 Django 包需要任何级别的集成测试,这也适用于不可安装的应用。
在哪里包含测试
当您向 Django 项目中的应用添加测试时,您可能会在每个应用中包含测试模块,或者是一个文件,或者是一个目录:
myapp/
__init__.py
models.py
tests.py
这个将也适用于独立的应用,但通常应该避免。在这种情况下,您的测试应该位于应用之外的一个单独的顶级模块中。如果你使用额外的模块进行测试,比如测试应用,那么这可以确保你的应用不依赖于代码中未安装的模块。它还保持安装的包更干净(尽管值得注意的是,这不是一致的意见)。
myapp/
__init__.py
models.py
test_app/
__init__.py
models.py
tests/
__init__.py
test_models.py
没有 Django 的测试
这里的重点是 Django 应用,即可以安装并包含在 Django 项目中的 Python 模块,以使用模型、模板标签、管理命令等。但在许多情况下,应用提供的功能可以作为普通的旧 Python 代码来测试。
这将是你的应用中需要设置的任何东西的情况,比如模型。然而,这并不适用于 Django 的每个部分或你的应用的每个部分。事实上,如果您的应用没有任何模型,并且您没有任何与请求相关的功能要测试——特别是在集成测试级别——那么您可以放弃设置或使用 Django 的测试模块,坚持使用标准库的 unittest,或者您选择的任何其他测试框架。
如果您正在加载 Django 项目,例如,任何涉及模型、设置或完整的请求/响应周期的东西,您只需要通过 Django 调用一个测试运行程序。在大多数情况下,测试表单、模板标签和过滤器中的逻辑等特性并不依赖于 Django 中任何需要项目设置的部分。
你为什么要这么做?使用 unittest 而不是 django.test 所带来的性能提升是非常值得怀疑的,更不用说有影响了。然而,如果这些是您唯一需要的测试,那么您的测试环境将更容易设置和运行。
摘要
在这一章中,你学习了为什么对你的独立应用进行测试是重要的,以及当 Django 应用不再是父 Django 项目的一部分时,如何开始测试它。您还了解了如何使用处理 Django 设置的 Python 脚本来简化测试执行,以及如何测试基于您自己的应用之外的其他应用定义的关系的应用特性。最后,您了解了将独立应用的测试包含在顶级测试目录中的位置,并且,对于某些不依赖于数据库或模板引擎的应用类型,使用 Python 的 unittest 库而不使用 Django 设置可能就足够了。
在下一章中,您将学习如何在没有 Django 项目的情况下为您的应用管理数据库迁移。
四、模型迁移
数据库迁移允许您跟踪数据库模型中的变化,并将它们传播到底层数据库模式。如果你的独立应用包含具体模型,那么你需要在你的应用中包含迁移。与运行测试一样,这在独立应用中与在您自己项目中的应用中没有本质上的不同;然而,有几个陷阱需要注意。
在这一章中,你将学习如何在你的 Django 项目之外管理你的应用的数据库迁移,以及一些使这些迁移更加安全和清晰的实践。
项目之外的迁移
当您为项目中的应用创建迁移时,只需从项目根目录运行管理命令即可:
./manage.py makemigrations app
对于迁移,我们会遇到与运行测试相同的问题——我们没有一个项目来运行迁移命令。
用于运行测试的 runtests.py 脚本可以适用于运行迁移命令;然而,采用现有的模式会更简单:每个 Django 项目附带的 manage.py 脚本。
在项目根目录中,创建一个 manage.py 文件。名字本身并不重要,但有了这个名字,它的目的对你和其他任何人来说都是显而易见的。就在 runtests.py 示例中,您可以直接从调用 settings.configure 或指向单独的设置模块的文件中配置 Django。最终结果看起来与标准 manage.py 脚本几乎没有区别。
import sys
import django
from django.conf import settings
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.admin",
"django.contrib.contenttypes",
"django.contrib.sites",
"myapp",
]
settings.configure(
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
}
},
INSTALLED_APPS=INSTALLED_APPS,
ROOT_URLCONF="tests.urls",
)
django.setup()
if __name__ == '__main__':
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
测试迁移
有时候,开发人员发布更新只是为了发现令人惊讶的失败构建,或者更糟糕的是,失败的部署,这都是因为没有添加或包含一个或多个迁移。这可以简单到改变一个字段的值,或者添加一个全新的模型和数据库表。
或者,更新可能工作得很好,但是所涉及的更改在当前模型的状态和迁移定义的状态之间产生了差距。更改模型字段的属性就足以做到这一点,即使它不需要对数据库模式进行任何更改。这种情况下的问题是,最终用户可能会自己创建缺失的迁移,这在应用时可能会与您添加到包中的后续迁移相冲突。
通过仔细检查没有可供迁移的更改,可以避免这两种情况。比双重检查更好的是,这可以添加到您的自动化测试套件中。
from django.test import TestCase
from django.core.management import call_command
class TestMigrations(TestCase):
def test_no_missing_migrations(self):
call_command("makemigrations", check=True, dry_run=True)
前面的测试所做的就是使用两个命令行选项运行 makemigrations 命令,- check 和- dry-run。如果检测到任何更改,check 标志会使命令以失败的非零状态退出,而预演标志只是确保不会创建其他输出。如果您缺少迁移,此测试将会失败。
附加迁移指南
如果您没有描述性地命名迁移的习惯,创建一个独立的应用是一个养成这个习惯的好机会。如果迁移足够简单,Django 将尝试提供一个描述性的名称,但这并不总是可能的,相反,您将只能进行带时间戳的迁移。虽然您确实可以阅读源代码,但从名称中了解其内容和用途会很有帮助。您可以使用-n 选项指定文件的名称:
./manage.py makemigrations myapp -n add_missing_choices
对于迁移名称,一个好的指导原则是将它们视为更简洁的 Git commit 消息主题:(I)进行了何种更改(例如,添加、更新、删除)以及(ii)迁移本身的主题。这将有助于您以后添加新功能,并且有助于参与者了解数据库更改的进度。
摘要
在本章中,您了解了如何利用用于项目外部测试的相同基本策略,为您的独立应用创建数据库迁移,如何将缺失迁移的测试添加到您的测试套件中,以及为什么建设性的迁移命名是有价值的。在下一章中,我们将会看到在你的独立应用中包含 HTML 模板,包括如何包含它们,但同样重要的是包含什么来优化对用户的有用性。
五、模板
在独立应用中包含 HTML 模板的机制与在 Django 项目的应用中包含模板没有什么不同。但是,您确实需要仔细考虑命名以及包含在模板中的内容。在本章中,您将学习如何为最终用户命名模板,以及如何为开发人员用户优化模板内容。
三个基本策略
如果您的应用包含返回呈现的 HTML 响应的视图,它将通过呈现 HTML 模板来做到这一点。由于 Django 加载模板的方式,您有三种选择来处理初始模板内容:
-
不要包括模板——毕竟,这是用户的网站。
-
包括基本的 HTML 模板。
-
包括详细的甚至是风格化的模板。
在大多数情况下,你应该在你的应用中包含基本的 HTML 模板,这些模板显示了开发过程中的渲染结果和你的视图中包含的模板上下文的结构。
包括什么
第一个选择,发布你的独立应用,但不包含你的视图所引用的 HTML 模板,应该被认为是不可行的。排除模板的主要好处是,它使你的应用的用户在哪里需要添加他们自己的模板变得很明显。然而,虽然应用的最终用户能够提供他们自己的模板,并且您可以记录应该包括哪些模板,但这增加了使用应用的摩擦。这使得探索性的使用变得更加困难,并使得在最终的模板中应该期望什么变得不那么明显。
第二个和第三个选项之间没有明显的区别,但是除了样式之外,我们可以确定一个更详细的模板,它引入了一些元素的组合,这些元素对于交付应用的功能并不是严格必需的。这个简短的模板,我们可能会认为是一个简单的应用视图,引入了一些不必要的假设。
{% extends "base.html" %}
{% block content %}
<h3>Here is a list of other fruits reported by the app</h3>
<ul>
{% for fruit in fruit_list %}
<li class="fruit-{{fruit.category }}">{{ fruit }}</li>
{% endfor %}
</ul>
{% endblock content %}
我们期望页面呈现如图 5-1 所示。
图 5-1
呈现的网页
虽然这是一个好的、流行的约定,但是没有什么要求要求任何人将基础模板命名为 base.html,也没有任何要求,如果这样的模板存在,它应该是这个特定级别的直接基础模板。同样,不要求任何项目模板都包含名为 content 的模板块。这可能是有意义的,并且可能是你在 Django 项目中见过的最一致的东西,但是这仍然是一个方便的约定。因此,虽然这个模板将为拥有 base.html 和内容块的人创造更丰富的初始体验,但对于那些没有的人来说,它将失败。
更好的策略——在大多数情况下——是包含基本模板,用最少的结构和样式向用户显示模板上下文的全部范围。下面是之前的例子:
<h3>Here is a list of other fruits reported by the app.</h3>
<ul>
{% for fruit in fruit_list %}
<li class="fruit-{{fruit.category }}">{{ fruit }}</li>
{% endfor %}
</ul>
图 5-2 中的结果并不令人兴奋,但是预期用户将会覆盖我们的
图 5-2
最少的结构和样式
模板最终会使它们在发布的应用中看起来不适合生产。
有些情况下,提供更详细的模板是有用的,例如,应用的业务目标是提供风格化的结果,定制的管理皮肤。
在你的模板中包含翻译字符串是一个好主意,但是对于简单的例子模板来说没有必要。考虑到您的开发人员用户将会覆盖这些,他们可以包含这个或他们选择的语言。参见第十三章了解如何解决这个问题。
电子邮件和杂项模板
从视图中呈现 HTML 响应同样适用于任何其他模板化的内容,包括电子邮件。电子邮件模板是应用中的一个常见功能,包括用户注册、邀请和任何其他类型的出站通知。
关于电子邮件和通知模板的一个新增功能是将它们包含在自己的 templates 子文件夹中,例如 email/。
Flat is Better than Nested: The Zen of Python
"使用额外的目录不是不必要的嵌套吗?"记住实用性胜过纯粹性,目录“仅仅”是名称空间。myapp/email/welcome_body.html 和 myapp/email_welcome_body.html 的命名空间深度差异为零;他们只是性格不同而已。查看文件夹系统的不同之处在于它们总是显而易见的,不仅在文件系统中,更重要的是在调用源代码中。
摘要
在这一章中,你学习了如何最好地在你的独立应用中包含 HTML 模板,以及在其中包含什么。您了解到,与其包含依赖于特定基础模板的大量样式的模板,不如只包含模板的核心结构来演示模板中的可用内容。在下一章中,您将学习如何包含像 CSS 和 JavaScript 这样的静态资产来提供基本样式和前端功能。
六、使用静态文件
在您自己的 Django 项目中,您可能有静态文件,包括样式表、JavaScript、字体和图像在内的静态资产,所有这些都打算直接提供给最终用户的浏览器。这些静态文件或静态资产允许您控制呈现的 HTML 的布局和样式,并引入客户端(浏览器)交互性。
虽然不太常见,但一些独立的应用可能会包含自己的静态文件,这意味着可以通过最终用户项目的名称直接引用,也可以通过您自己的应用模板引用。各种类型的独立应用可能使用静态文件,通常包括提供自定义管理功能的应用和捆绑前端框架组件的应用。在本章中,我们将逐步介绍在独立应用中包含静态资产的意义以及如何包含它们。
独立应用中的静态文件
历史上,在可重用的应用中包含静态文件有两个主要原因:
-
添加应用提供的基于界面的功能,例如,一个更大整体的核心组件。
-
将静态文件包含在项目生成过程中,例如,包含 JavaScript 框架,以便在运行 collectstatic 时,所需的框架文件自动可用于项目。
后一个原因,包括项目中的 JS 框架这样的静态文件,在很大程度上已经过时了。随着前端应用代码和构建管理系统(如 Gulp、Webpack 和 package)的流行,开发人员和团队从这些构建系统中包含和构建这些文件,与 Django 静态管道并行,这种情况要常见得多。
例如,如果应用的目的是将 Vue.js 框架包含在项目模板中,那么避免创建 Django 应用并配置项目 JavaScript 构建系统(如 Webpack)来直接包含框架将是一个更好的主意。除此之外,这使得对 JavaScript 依赖版本的细粒度和可移植控制成为可能。
这并不是说没有创建可重用应用来提供这种功能的用例。对于不保证 JavaScript 构建过程或非常狭窄的用例的较小的内部项目,这仍然是有益的。
相反,在独立的 Django 应用中包含静态文件的主要用例是包含基于接口的功能或样式。包含静态文件的机制很简单:
-
在应用目录中添加一个静态/目录。
-
将静态文件添加到新的静态/目录中。
Note
我们还有一个步骤来确保这些文件包含在最终的包中进行分发,但是这足以填充项目。如果最终用户启用了基于应用目录的静态文件收集(默认情况下包含),则在安装了应用的项目中和 INSTALLED_APPS 中运行 collectstatic 会将这些文件复制到项目的 STATIC_ROOT 目录中。
STATICFILES_FINDERS = [
...
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
值得注意的是,当您运行 collectstatic 命令来收集项目的所有静态文件时,所有文件都将被聚合到项目的 STATIC_ROOT 目录中,包括它们相对于应用静态目录的路径和。这意味着,如果您包含 static/style.css,它将作为/static/style.css 包含在最终项目中。这不仅模糊了文件的来源,还会导致命名冲突。
与模板一样,解决方案是包含一个命名子目录来命名文件:
myapp/
static/
myapp/
style.css
templates/
myapp/
list.html
现在,您的文件将作为/static/myapp/style.css 提供。如果最终用户聚集并缩小这些文件,您的资产的最终名称可能并不重要,但您总是希望避免命名冲突。
在 Django admin
如果您的 Django 应用包含任何类型的 Django admin 可视化定制,从全面的样式替换到较小的 JavaScript 小部件,您可以像前面描述的那样包含必要的文件。这些文件可以包含在同类的基于目录的名称空间中,或者使用 admin/子目录。这是分割较大文件集合的好方法,尽管为了避免名称冲突,您应该确保文件具有特定于应用的名称和/或使用额外的基于目录的命名空间。
此布局将包括 myapp.css 作为/static/admin/myapp.css:
myapp/
static/
admin/
myapp.css
myapp/
style.css
然而,没有必要为这些文件使用 admin/namespace,尤其是如果您的应用只包含特定于管理员的静态文件。另一方面,如果你的应用的目标是覆盖或者取代现有的管理文件,那么你应该命名它们,使得文件路径与你想要覆盖的文件完全相同。
myapp/
static/
admin/
css/
login.css
现在假设 myapp 包含在项目的 INSTALLED_APPS 中的 django.contrib.admin 之后,那么它的 login.css 文件将被用来代替 django 的文件。
最后,如果您在 Django admin 中包含了基于 jQuery 的 JavaScript,请确保您的插件或函数与您想要支持的 Django 版本兼容。如果您这样做,并使用 Django 管理员使用的特殊命名空间——Django 的管理员为其包含的 jQuery 使用django.jQuery
命名空间以避免与其他引入的版本冲突——您就不需要自己包含 jQuery。另一方面,如果你的定制功能由于某种原因依赖于一个非常特定的 jQuery 版本,你会希望在你的应用中包含那个版本。在这种情况下,您可以效仿 Django,使用“普通的”jQuery 名称空间或您自己的名称空间,以避免与任何其他引入的 JavaScript 发生任何后续冲突。
摘要
在本章中,您已经学习了如何在您的独立应用中包含静态资产,在您的独立应用中包含什么样的静态资产是有意义的,以及如何为特定于 Django admin 的功能包含 JavaScript 资产。在下一章,我们将进一步探讨命名空间的问题,激发挑战,并建立一套创建合理和一致的命名系统的策略。
七、应用中的命名空间
名称空间是一个非常棒的想法——让我们做更多这样的事情吧!
—蟒蛇之禅
在前几章中,我们介绍了如何向您的应用添加一些功能,包括 HTML 模板和静态文件,它们是通过使用应用命名空间来组织和访问的。
在这一章中,我们将了解命名空间的决定是如何渗透到你的独立应用的其余部分,以及如何利用命名空间来使你的应用的集成和使用更加容易。
名称空间一览
名称空间是一种组织命名的对象的方式——在 Python 和其他语言中——这样它们既有一个父标识符,更重要的是,它们的名称不会冲突。我们已经讨论过几次名称空间,主要是在模板和静态文件目录的上下文中,但是它们的使用远远超出了配置目录以收集文件的范围。
带命名空间
-
两个不同的模块可以各自定义一个名称相同的函数,这样 my_module.does_a_thing 和 your_module.does_a_thing 就不会冲突。
-
两个不同的类可以各自定义一个名称相同的方法,这样 MyClass.does_a_thing 就不会与 YourClass.does_a_thing 冲突。
-
两个不同的字典可以各自包含映射到不同值的相同键,例如 my_dict["key"] = 4,而 your_dict["key"] = 1。
在您自己的项目中,使用不同的名称空间很容易被认为是理所当然的,但是在将代码引入不同的 Django 项目时,您应该花时间确保它有合理的名称空间。
应用本身
我们命名一个独立的 Django 应用的切入点是应用本身,更确切地说,是它的模块名和它在 AppConfig 中的命名方式。这是最简单也是最重要的一步。为了避免 Django 项目代码库中的名称冲突,更重要的是,为了使区别更明显,应用名称应该是描述性的,并且不能明显地与已知的现有应用名称重叠,无论是 Django 附带的名称还是为共享而发布的名称。
例如,如果您构建了一个独立的应用来与 stripe 计费服务进行交互,您可能会忍不住将其命名为 Stripe。但是如果这样做,就会与权威的 Stripe Python SDK 发生冲突。相反,你可能会决定把它命名为与 Django 相关的东西,比如 django_stripe 或 djstripe,只是现在后者至少与现有的已发布的独立应用有冲突!如果你的应用是 djstripe 功能的替代品,那么它们不太可能在同一个项目中使用,因此会发生冲突;然而,除非你的应用是 djstripe 的一个分支,否则它很可能会给使用该应用的开发人员带来困惑。在这种情况下,请选择不同的名称。
当一个应用的描述性名称因为冲突而不可用或不明智时,选择一个具有额外上下文的适应名称,如 stripe_billing,或使用同义词或典故,也可以,如 zebra。斑马应用现在是一个无人维护的应用,用于在 Django 项目中集成条纹支付,这样命名是因为斑马有条纹。
资源定位符
有多种方法可以将名称空间添加到 URL 中,以使它们在项目中易于识别,并避免命名冲突。在命名冲突的情况下,将使用第一个匹配的命名的 URL 。这可能会令人困惑,尤其是在没有引发异常的情况下。
完全可以使用基本 URL 名称本身来建立名称空间。myapp_list 和 myapp:list 之间基本上没有什么差别。后者更清楚名称空间在哪里“中断”,但两者都确保了与列表相关的视图对于 myapp 名称是唯一的。
设置
如果你的独立应用允许通过 django.conf.settings 进行配置,那么这些也需要一致的命名空间。在您自己的 Django 项目中可能适用的东西并不能保证在所有其他项目中都适用。
例如,对于一个名为 organizations 的应用,它管理多个用户的帐户,您可能有几个设置来控制要使用的用户模型、每个群组允许的成员数量以及管理员用户是否可以邀请新成员:
GROUP_USER_MODEL = AUTH_USER_MODEL
GROUPS_LIMIT = 8
ADMINS_CAN_INVITE = True
虽然在最初的 Django 项目中已经足够混乱,但是影响的范围非常有限。然而,在一个可重用的应用中跨项目传递这些混杂的名字解决了这个问题。因此,请确保项目设置中命名的每个设置都有一个一致的前言,例如:
ORGANIZATIONS_USER_MODEL = AUTH_USER_MODEL
ORGANIZATIONS_USER_LIMIT = 8
ORGANIZATIONS_ADMINS_CAN_INVITE = True
有关在应用中构建设置和处理默认值的更多信息,请参见第七章。
管理命令
Django 的管理命令作为 Django 项目的基于命令行的接口。考虑它们的一个好方法是视图,但是用于终端处理而不是 HTTP 请求。独立应用包含管理命令有很多原因:同步数据、导入和导出数据,或者提供创建默认数据的方法。
快速回顾一下,管理命令名来自模块(文件)名。Django 会将定义 BaseCommand 子类的管理/命令中的模块视为命名管理命令。
myapp/
__init__.py
management/
__init__.py
commands/
__init__.py
migrate_user_data.py
然而,与 URL 名称不同,管理命令名称是全局的。如果您想包含一个管理命令,该命令将跨系统迁移用户数据,使用名称 migrate 将与 Django ORM 的 migrate 命令冲突,覆盖基本的 migrate 命令会引起很大的麻烦。
为了避免名称冲突和明确命令的目的,有两种解决方案:
-
在命令名(模块名)前加上应用标识符
-
使每个命令名尽可能具有描述性和唯一性
使用应用名作为管理命令的名称空间并不是一种常见的做法,但这并不意味着这不是一种好的做法。如果你的应用有几个管理命令或者你的应用有一个简单的名字,这是一个好的策略。django-jet 和 dj-stripe 都遵循这种做法,分别在管理命令前面加上 jet_ 和 dj_stripe。在 dj-stripe 的情况下,这意味着 sync_models 的命令被明确定义为与 dj-stripe 相关,而不是全局模糊的命令。
在管理命令的情况下,即使没有名称前缀,使命令名称显式通常也就足够了。这可能包括在其他地方包含应用名称,或者引用应用特有的内容,如一类数据或服务。django-cachalot 提供了 invalidate_cachalot 管理命令,这显然是一个特定于应用的名称,而且它的功能也很清楚。django-reversions 提供 createinitialrevisions 和 deleterevisions。
如果没有类似 URL 的管理命令命名空间方案,您选择哪种策略将取决于上下文。
模板标签
在 templatetags 包中放置多少模块没有限制。请记住,{% load %}语句将加载给定 Python 模块名称的标签/过滤器,而不是应用的名称。
—Django docs
模板标签和过滤器在渲染时向模板添加逻辑和格式功能。添加新的模板标签非常简单,只需在应用中包含一个 template tags 模块,然后将一个或多个标签库作为子模块。
myapp/
templatetags/
__init__.py
myapp_tags.py
模板标签和过滤器提出了两个命名空间挑战:
-
标记库名称是全局的,也就是说,不是与应用相关的名称空间。
-
单个标签和过滤器被类似地加载到单个 namesapce 中,尽管只是在加载库的模板的上下文中。
这意味着您应该使用名称前缀来保持您的模板标签模块的唯一命名,并命名各个标签和过滤器,以便它们隐式地应用命名空间,至少在它们提供某种特定于应用的功能时。如果标签或过滤器的用途或用途超出了应用中数据的上下文,那么更通用地命名它们可能更有意义。
模型和数据库表
由于应用名称本身的原因,应用模型及其各自的数据库表名称都有默认的名称空间。一个项目可能有 15 个不同的应用,每个应用都有自己的模型类 User,这不会造成任何特殊的冲突,只要它们是用别名导入的,它们可能会发生冲突。
from app1.models import User as UserApp1
from app2.models import User as UserApp2
from app3.models import User as UserApp3
尽管如此,导入冲突并不是我们在命名和描述性方面寻求避免的唯一问题。如果一个模型既以特定于应用的方式提供服务(即,很难解释如何在应用之外使用它),又希望在应用之外与其他模型一起使用,则应使用特定于应用的命名。
class MyAppUser(models.User):
"""A user model specific to the myapp app"""
数据库表命名在开发 Django 项目时基本上是事后才想到的,因为 ORM 会生成默认的表名。虽然关于创建描述性的和人类友好的表名还有一些要说的,但是对于一个可重用的应用来说,主要关心的只是表名中的应用前言应该是唯一的。
假设你有一个应用,它提供了一种面向用户的日志记录,你把这个应用命名为 logs 对于一个独立的应用来说,这不是一个很好的名字,但让我们把它当作一个既定的。数据库表名将以 logs_ 开头,例如,对于名为 LogEntry 的模型,表名将是 logs_logentry。
如果您在 Django 应用之外编写任何 SQL,那么表名缺少源代码范围内模型类所具有的上下文。因此,在本例中,如果您必须维护应用名称日志,那么在您的模型元类中指定 db_table 值是明智的:
class LogEntry(models.Model):
...
class Meta:
db_table = "activitylogs_logentry"
现在,任何编写 SQL 查询或检查数据库的人都会更清楚地了解这个表代表什么以及它包含什么类型的数据。
八、创建基本包
获取 Django 应用并使其独立的最后一项工作是将它变成一个可安装包。这本身是一个丰富的话题,我们将在第十八章中更深入地讨论,但目前我们的目标是满足最低要求,使一个简单的 Django 应用可以从 Django 项目之外安装。
示例博客应用
这个简单的博客应用已经被用于无数的教程和示例,否则它会变得陈旧,在这里它让我们专注于作为一个工作的独立的应用的新功能。
我们将在第四部分中更详细地介绍如何设置一个包,但是这将足以创建一个可测试、可部署和可发布的包。我们的博客应用非常简单,只包括一个模型,Post,两个视图 post_list 和 post_detail 及其各自的 URL,一个用于呈现阅读次数的模板过滤器,两个视图的基本模板,以及一个用于初始博客样式的 CSS 文件。
blog
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
├── models.py
├── static
│ └── blog
│ └── blog.css
├── templates
│ └── blog
│ ├── post_detail.html
│ └── post_list.html
├── templatetags
│ ├── __init__.py
│ └── blog_tags.py
├── urls.py
└── views.py
除了用于运行测试和创建迁移的文件之外,我们将在根 blog_app 目录中包含 blog app 目录。
blog_app
├── blog
├── manage.py
├── runtests.py
├── setup.py
|-- tests
我们在这里包括的一个东西是 setup.py 文件。
基本的 setup.py 文件
为了对此进行打包,我们需要一种定义包的方式:它叫什么,它是什么版本,代码在哪里。如果您熟悉 Ruby Gemspec 或 Node package.json 文件,setup.py 文件有类似的作用。而且只是 Python。我们来看看文件。
from setuptools import setup, find_packages
setup(
name="blog",
version="0.1.0",
author="Ben Lopatin",
author_email="[email protected]",
url="http://www.django-standalone-apps.com",
packages=find_packages(exclude=["tests"]),
)
这是我们能得到的最基本和最简单的东西。这不足以发布我们的包,但应该足以构建它,以便我们可以将它作为独立的 Python 包安装在本地。
setup 函数的参数顺序没有意义,因为它们是关键字参数,这里对它们进行分组和分隔只是为了便于解释:
-
第一个参数是包名。如果忽略这一点,您仍然可以为您的包创建一个构建目录,但是任何构建工件,从 wheel 文件到压缩的源代码,都将被命名为 UNTITLED,从而避免在其他地方发布和安装。
-
第二个参数指定了版本号。这对于在发布错误修复或新功能时替换旧版本至关重要。
-
作者的名字,也就是你。
-
作者电子邮件,这是你的电子邮件地址。
-
项目 URL,表示某人可以在哪里找到关于项目的更多信息(例如,文档站点、源存储库)。
-
最后一行指定了在哪里可以找到包。这是一个关键的参数,通过依赖 find_packages 函数,我们可以避免指定单独的路径名。
使用该文件,我们可以运行 python setup.py 检查并查看我们没有遗漏任何内容,然后运行 python setup.py build 来生成包的副本,因为它将以分布式形式出现在 build/目录中。
添加模板和静态文件
如果使用 python setup.py build 命令构建项目,并在新创建(或更新)的 build/目录中列出文件,您会发现缺少模板和静态资产文件:
build/
lib/
blog/
__init__.py
admin.py
apps.py
migrations/
0001_initial.py
__init__.py
models.py
templatetags/
__init__.py
blog_tags.py
urls.py
views.py
这是因为 setuptools 只寻找包含在您的包中的 Python 文件(以及一些特定的非 Python 文件)。为了包含这些,我们需要包含一个清单文件,即 MANIFEST.in。
MANIFEST.in 文件允许您使用非常简单的命名或通配符格式来指定应该包含在您的包中的文件。在我们的例子中,我们不希望必须分别指定每个模板和静态资产,所以我们希望使用通配符。为了举例,因为我们只有一个 CSS 文件,我们将使用两个。
include blog/static/blog/blog.css
recursive-include blog/templates ∗.html
这两行根据相对于根目录的位置来指定文件,即父目录中的 MANIFEST.in。第一行包含一个文件的路径名,而第二行将包含位于 blog/templates 目录中的所有 HTML 文件,包括子目录。匹配这些行的文件将与 Python 文件一起被复制到构建产品中。
现在,如果您使用 python setup.py build 构建您的应用,您会发现您的static
和templates
目录包含您所有的 CSS、JavaScript 和 HTML 模板:
build/
lib/
blog/
__init__.py
admin.py
apps.py
migrations/
0001_initial.py
__init__.py
models.py
static/
blog/
blog.css
templates/
blog/
post_detail.html
post_list.html
templatetags/
__init__.py
blog_tags.py
urls.py
views.py
安装和使用
现在有两种方法可以安装一个可用的本地应用。您可以运行 python setup.py install,它会将应用的副本安装到与当前 python 路径相关的 site-packages 目录中(例如,system Python site-packages 或 virtualenv site-packages)。或者可以运行 python setup.py develop。这将安装一个从 site-packages 到您的项目根目录的链接,链接的形式是一个名为 blog.egg-info 的文件,其中包含您的包的路径。
使用 develop 命令的好处是,由于你的包目录被有效地用符号链接到你的站点包中,所以对你的包的每一个修改都可以在你使用这个包的任何地方立即得到。因此,与另一个项目并行开发一个包(这里是您的独立应用)变得更加容易,而不必每次更改都重新安装。
不利的一面是,你最终可能会开发另一个独立应用的未标记版本(即特定版本)。那么风险就是你的另一个包不能准确地捕捉你的独立应用的特性或发布的 API。如果您尝试使用 python setup.py develop 在 Python 环境中安装应用,然后使用 pip 进行安装,也可能会遇到冲突。因此,这应该用于探索性工作,而不是作为安装独立应用的全职策略。
在第十八章中,我们将深入研究一些改进的策略来构建一个使维护更容易、发布共享更简单的包。
摘要
在本章中,您了解了为独立应用创建可安装 Python 包的基本结构和要求。这为在 Django 项目环境之外使用您的独立应用提供了最基本的第一步,并为您在包索引上发布提供了基础。在下一章,我们将开始研究如何从你现有的 Django 项目中评估和提取一个独立的应用。
九、范围界定和绘制边界
在第一章中,我们简要讨论了界定 Django 独立应用的范围意味着什么,包括你创建独立应用的目标,对第三方依赖的考虑,以及应用预期执行的工作。
在这一章中,我们将更深入地回顾这些相同的考虑,特别是关于在现有代码库的上下文中寻找一个可能的 Django 独立应用。
问题的范围和性质
定义软件项目的范围是项目的首要挑战之一,也是最重要的挑战之一。范围定义了它将做什么,边界在哪里,并且不仅影响项目的代码行大小,还影响它的复杂程度,它可以支持多少功能,以及测试和维护软件的挑战性。
在您自己的项目中编写的应用可能有一个明确的范围,但是当它是项目源代码的一个集成部分时,边界很容易变得模糊。这可能是因为功能蠕变或某些即时便利——在应用中包含一些新的小功能更简单或更方便,即使该功能的目的与应用的核心工作正交,或过于特定于您的特定项目。
应用的范围也将影响需要什么依赖关系。随着“依赖表面积”的增加,应用在维护和版本支持方面的脆弱性也在增加。如果你的独立应用依赖于 Django,那么它只需绑定到 Django 版本。然而,如果它需要 Django 和一个或多个附加的独立应用,那么它很可能会受到所有依赖项在支持 Django 和 Python 版本时重叠程度的限制。从你自己的项目中减少你的独立应用的范围,预期的效果是使它更容易维护。
独立应用的工作
- 你说你在这里做什么?
如果你在寻找一种启发式的方法来界定应用的范围,你可以做得比简单地写一份简短的工作描述更糟。作为一名员工,你可能有一个不合适的职称和众多不同的职责,但如果你把它剥离,那么很可能你可以在你所做的事情之外为你的工作定义一个目标。作为一名软件开发人员,你的工作是将业务需求转化为工作软件。作为首席执行官,你的工作是领导公司实现增长和盈利(至少在大多数地方是这样)。可能会有更多的工作职责,这些职责可能很重要,但是几乎每份工作都有一个单一的目的。同样,你的独立应用应该有一个指导性的目的,可以简单明了地描述。
设身处地为营销人员着想(不,真的)。你会如何描述你的应用?它解决什么问题?它是如何解决这个问题的?对于许多其他应用,它是如何解决这个问题的?
这些问题的答案将有助于开源软件包的营销,但这不是我们问这些问题的原因。
以下是几个流行的独立 Django 应用,用我自己的话来说,是它们的简短工作描述:
-
Wagtail 的工作是以一种用户友好的方式使用户可编辑的内容更容易建模和服务。
-
Django REST 框架的工作是为创建 RESTful APIs 和将现有的 Django 应用连接到 API 提供类似 Django 的体验。
-
django-taggit 的工作是在分类学的意义上,使向 django 项目中的任何类型的对象添加标签变得容易
-
Easy Thumbnails 的工作是为单个上传的图像创建调整大小的图像。
这些应用的大小和它们提供的功能范围各不相同,但在每种情况下,所有这些功能都可以追溯到一个非常明确的目的。
创建和提取的维度
你可以从几个方面来看待一个独立的应用;在这里,我想把重点放在我称之为垂直和水平细分的轴上,或者换句话说,业务功能与技术基础。在下图中,我们看到一个特性(黄色条)是通过任何应用或项目的水平组件的垂直切片(图 9-1 )。
图 9-1
垂直和水平分割
不要被说服这允许某种科学分类;相反,这是一种评估模块如何分解和组织的便捷方式,在独立的 Django 应用环境中,如何定义它们的工作。
水平分割的模块——应用或其他——提供了某种通用的“基础设施”,可以在支持功能开发的项目中使用。这是一段只有开发人员才能体验到的代码(总的来说)。
例子包括
-
django-model-utils
-
django-扩展
-
Django-克里西弗斯
这些主要解决开发团队面临的问题,比如使复杂表单的呈现变得更容易,提供公共基类以避免重复的样板代码。
项目中水平组织的代码看起来像这样,强调通过代码首先做什么以及它其次解决什么业务领域来组织:
app/
forms/
models/
...
image_models.py
user_models.py
subscription_models.py
views/
这看起来很熟悉,因为这是在 Django 应用中组织代码的惯例,但也是在其他框架中组织项目代码的惯例。
另一方面,垂直分割的模块首先是围绕业务需求组织的,也就是说,通常是用户将会经历的事情。
例子包括
-
决哥,请回答
-
干草堆
-
Django-沙阿
显然,这不是一个“非此即彼”的区别。但 Django 以应用为中心的架构鼓励垂直或基于功能的组织。Django 应用包括从模型到 URL 路由到表单和视图的所有内容。这必然是独立应用发布的方式,但这也是 Django 项目中应用的默认模式。
如果你的目标功能看起来像是与功能无关的东西,它可能需要包含在一个独立于功能的应用中。
确定应用的范围
一个独立的 Django 应用应该足够大以完成它的工作,而不是更大。但是那有多大呢?错误地确定独立应用的范围会有什么后果?你如何解决一个看起来太小或太大的应用?
首先,一个应用不应该比它的工作大。但也要足够大。如果它太大,要么它做得太多,要么它本身就是另一个框架。太小,它可能不保证创建一个独立的应用,甚至是一个可安装包。
当应用太大时
一个 app 太大意味着什么?有一些相当大的独立应用不一定太大。答案一清二楚:看情况。让一个应用变得太大的第一件事是包含无关紧要的功能,这些功能要么对应用不重要,要么在他们的自己的应用中足够有用。一个容易处理的例子是一个包含自己的工具基类的应用,就像你在 django-model-utils 中找到的那种。这些额外的功能可能会在维护应用和添加功能时分散注意力。它们也增加了需要测试的代码量。如果有必要,不要抛弃它们,但要确保它们是必要的。
对于太大的应用,有几个突出的解决方案:
-
把它分成单独的包裹。
-
组织成子应用。
如果至少有一个潜在的包本身就足够有用的话,那么分解成单独的包是一个好主意。在这种情况下,独立包的好处是可以为更多的用例提供更广泛的服务。然而,无论是否是独立的应用,每个额外的包都会增加维护成本,并可能使确保两个组件继续良好合作变得更具挑战性。
如果它们是相互依赖的,那么创建单独的包没有好处,在一个包中使用子应用或迷你应用是更好的选择。
将独立应用组织成子应用需要将每个应用单独添加到目标项目的 INSTALLED_APPS 中,如下所示:
INSTALLED_APPS = [
...
"cms",
"cms.pages",
"cms.photos",
...
]
这种策略包括一个主要的顶级应用,以及附属和组成功能应用,作为它们自己的可安装应用。虽然比只添加一个“核心”应用要笨拙,但这可能允许您更好地逻辑构建组件,并允许开发人员用户排除他们不需要的功能。这不会减少你的应用的源代码,但它确实减少了任何使用应用的人必须考虑的范围。
当应用太小时
制作一个太小的应用的问题与制作一个太大的应用的问题是不同的,可以说没有那么重要。主要的问题是,它有导致大量微小依赖的风险,这些依赖都需要被包含和维护。当独立包的数量超出了你容易记住的数量时,使用独立包的好处就减少了。我们希望尽量减少我们的应用对其他人的项目的依赖性。
如果你打算创建多个小应用,首先考虑它们是否真的有意义,或者它们是否在功能或业务领域有足够的通用性来捆绑在一起。如果您发现自己在不止一个项目中同时使用这些特性,很可能就是这种情况。
不过,创建一个小小的独立应用不应该被视为禁止。在光谱的两端有明显的张力。
摘要
在本章中,您学习了如何确定应用的范围,包括定义应用要完成的工作、评估应用可能的功能和组件维度,以及确定如何评估和调整范围的大小。在下一章,我们将学习如何从现有的 Django 项目开始重构和提取你的应用。
十、分离您的应用
虽然有些软件包完全以独立的可安装库的形式出现,但更典型的是以一种或另一种形式作为现有项目中的功能出现。这可能分布在整个项目中,也可能在项目的应用中。
本章的目标是从一个或多个应用中提取功能,要么移除一个整体,要么整合多个应用中的功能,并将其放入项目中一个独特的、与项目无关的应用中。这不仅意味着它有自己的应用,还意味着它“不知道”也不依赖于你最初的 Django 项目的细节。
入门指南
例如,这可能意味着从软件即服务(SaaS)项目中提取和删除特定的订阅计划信息。这意味着只使用 Django 的核心设置或特定于应用的设置。这通常意味着放弃特定后端服务的假设,如电子邮件提供商或云文件存储提供商,这种假设对于应用的核心功能是不必要的。
所有这一切的关键是理解你的应用(??)目前在你的项目的依赖层次中的位置,以及它们应该在那个层次中的位置。依赖层次描述了 Django 项目中各种模块之间的相互关系(它可以用于任何软件项目,甚至是一个单独的 Python 包),以及相互之间的依赖关系。它包括您即将独立的应用、特定于项目的 Django 应用、其他第三方独立的 Django 应用、Django 本身、与 Django 无关的第三方 Python 包,甚至 Python 标准库。
- 这里是建筑图。我们想展示你在拉基础应用,或者从上面看,这取决于什么。
因此,我们的目标是,如果有必要的话,将 app 提升到依赖关系的层次上,这样它就可以在外部 Django 项目中使用。
首先重构
任何时候,当你回到甚至开始写代码的时候,都会有重写或编辑源代码的诱惑,或者通常所说的重构。然而,就其原始含义而言,重构意味着只修改代码,而不影响其工作方式,或者用马丁·福勒的话说(着重号后加):
重构是改变 软件系统的过程,其方式是不改变代码的外部行为,而 改进其内部结构 。
—重构,第 1 版。
马丁·福勒
这个术语的口语含义要宽松得多,通常用来描述对源代码的任何类型的编辑。然而,这里我们关注重构的更严格的定义。这包括从重新格式化源代码到重命名变量,直到分解或移动函数、方法、类甚至模块。它不涉及添加新功能或替换算法。这些可能是好的,甚至是必要的步骤,但它们不是重构。
尽管在重构方面使用了诱惑这个词,但重构是一件好事。它往往使代码更容易理解,也更容易重用。这些都是提取独立应用的好处。然而,在某些情况下,如果它掩盖了代码中进行的其他更改,它可能会成为一种干扰,所以当前面有估计的重构级别时,首先解决它是有意义的。这就解决了这个问题,并使代码更容易处理更重要的工作。
代码格式化——从标准化的缩进到包导入的组织——是一个很好的起点。代码中的差异有助于跟踪您的进展,如果它们充满了虚假的变化,就没那么有用了。如果你决定使用像 black 这样的自动套用格式软件,而你以前从未使用过,那么它的第一次运行可能会产生很大的差异。首先将它隔离为一个提交,然后继续前进,这样您就知道您做了哪些更改,哪些只是清除了空白。
接下来,您可能想要重命名一些变量或函数,特别是如果它们的命名方式过度反映了更大的 Django 项目。将此重构作为第一步的第二个原因是,对函数和类名、函数签名或类初始化签名的更改可能会级联到您现有的 Django 项目中。尽可能多的预先加载这些工作是有好处的,这样后续的更新可以尽可能的集中在应用本身。
模型重命名和迁移
重命名模型和/或数据库表是一项重构工作,但是它伴随着一些特定于 Django 的警告。这是因为对 Django 模型的类名甚至模块名的更改会影响迁移状态,当没有显式指定表名时,还会影响默认的数据库表名。以这种方式不小心更改表名可能是破坏性的。
第一步是确保你的应用模型使用一个公共的、合理的数据库表名称空间。您现有的应用名称可能不适合独立的应用,也有可能您的模型是在项目中的另一个应用中诞生的。因此,表名可能是不一致的,并且它们可能是无意义的描述。
因此,对于应用中的每个模型,确保其当前表名在模型的元类中明确命名:
class LogCategory(models.Model):
class Meta:
db_table = "tracking_logcategory"
class Entry(models.Model):
class Meta:
db_table = "someapp_entry"
此时,您应该创建一个迁移文件来捕获这种状态更改,尽管此时它对数据库没有任何影响(因为名称实际上没有改变):
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AlterModelTable(
name='entry',
table='someapp_entry',
),
]
第二步是根据需要使用 Meta.db_table 属性重命名这些表:
class LogCategory(models.Model):
class Meta:
db_table = "tracking_logcategory"
class Entry(models.Model):
class Meta:
db_table = "tracking_entry"
同样,为这个变更创建一个单独的迁移文件。运行时产生的迁移将改变底层数据库表名,但不会影响数据库的结构。
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AlterModelTable(
name='entry',
table='tracking_entry',
),
]
此时,您可以在应用中自由地重命名模型类,而不会影响数据库。
- 一点:如果您在应用的任何模型上定义了多对多字段,您应该确保您有一个显式定义的 through 模型,其表名在前面的示例中定义。您还需要从现有的表名开始。
如果您在应用之间移动模型类——例如,提取一个特定于项目的模型或者合并几个模型——您可能会遇到的另一个难题是,当模型改变应用时,Django 迁移是具有破坏性的。如果我们想将一个模型移出一个应用,下面是结果迁移:
class Migration(migrations.Migration):
dependencies = [
('myappp', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Entry',
),
]
该迁移操作是一个删除操作,如果运行,将导致删除底层数据库表。当然,有各种方法可以解决这个问题。你可以假装迁移,跑步。/manage . py migrate myapp 0001-fake,例如,在不影响数据库的情况下推进迁移状态。这随后需要为目标或接收者应用执行,可以说在本地开发中有点麻烦。在生产部署中尝试编排是非常讨厌的。
您还可以子类化 migration operation 类,使其 database_forwards 方法不做任何事情,从而不会影响数据库中的任何更改:
class DeleteNothing(migrations.DeleteModel):
def database_forwards(self, *args, **kwargs):
"""Do nothing"""
pass
这无疑优于伪造迁移,但同样麻烦,可能令人困惑,而且谢天谢地是不必要的。这是迁移的一个用例。SeparateDatabaseAndState 操作类。
像我们的 DeleteModel 这样的数据库迁移有两个影响:一个是对数据库的影响,这是我们试图防止发生的,另一个是对应用模型的累积状态的影响。我们确实需要后者。迁徙。SeparateDatabaseAndState 类允许您将这两者分开,以便可以运行影响迁移的状态。结果是更新的迁移状态“知道”表名是什么,并且不会对底层数据库产生任何影响。
实现这一点很简单;我们在顶级 Migration.operations 中插入类初始化调用,然后将删除操作移到 state_operations 关键字参数中,以分隔 DatabaseAndState。先前的迁移变成了这样:
class Migration(migrations.Migration):
dependencies = [
('myappp', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='Entry',
),
]
)
]
当运行时,这将推进 myapp 迁移历史的状态,以便入口模型不再是应用的一部分,但是它不会对数据库进行任何更改。
允许定制
很可能你的应用基于你对它的使用做出了一些假设。这些可能包括特定的支持服务或者甚至是工作流,它们接近你在另一个项目中所需要的,但是仍然足够具体,可以作为共享功能使用。有几种方法可以解决这个问题,虽然现在没有必要去做,但是在你的工作项目中进行这些改变是有利的。
后端类
您可能使用过提供定制功能选项的独立应用,包括
-
django-anymail(电子邮件)
-
django-allauth(认证)
-
干草堆(搜索)
其中的每一个都解决了一个特定的业务问题,这个问题有多个解决方案。在上述应用的情况下,这是通过允许开发人员用户选择特定的后端或提供商来实现的。与 Django ORM 中的数据库支持一样,每个定义的后端或提供者处理集成所需的细节,但是向开发人员用户公开一个公共接口以获得无缝体验。
使用这个选项的关键是每个后端或提供者类都从基类继承或者匹配一个基本接口。如果您的应用应该管理工作流,但当前有一个非常特定于项目的工作流,则可以将此工作流移动到项目中的一个单独模块中,并通过应用中的导入来引用。
在项目设置中,您可能需要指向类或模块的点状路径:
MYAPP_WORKFLOW = "core.workflows.CustomerWorkflow"
然后,在您的应用中,当您需要启动此工作流时,您可以简单地通过路径导入正确的类或模块:
from django.conf import settings
from django.utils.module_loading import import_string
def get_myapp_workflow():
"""Returns the class by dotted path from settings"""
import_string(backend or MYAPP_WORKFLOW)
def run_workflow():
"""Calls the user/project defined class"""
workflow_class = get_myapp_workflow()
customer_workflow = workflow_class().start()
...
信号
Django 的信号提供了一种分派命名事件的方法,并使用同步回调函数来处理它们。原型例子包括 Django 在 ORM 对象的生命周期里程碑发出的内置信号,包括 pre_save、post_save、pre_delete 和 post_delete。
信号允许您从特定的类对这些事件做出响应,并修改正在讨论的对象,或者作为特定操作或参数的结果触发一些其他工作流。当过度使用时,信号可能会令人困惑,使程序的流程变得混乱,使调试、测试、管理性能以及整体维护变得更加困难。也就是说,它们确实解决了当你知道无法访问原始代码时,如何改变某个对象的处理方式或者某个函数应该如何工作的问题。例如,在您自己的项目中,向模型类的 save 方法添加一些语句是微不足道的;当使用独立的应用时,这变得不可行。
class Entry(models.Model):
def save(self, **kwargs):
some_webhook(self)
return super().save(**kwargs)
特别是如果自定义功能看起来像是一次性的,或者是一种将一个模型中的更改绑定到另一个模型的方式,以便它遵循 Django 自己标记的相当标准的模式,signals 可以帮助您从应用中解开特定于项目的逻辑,并将其保留在您的项目中:
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import Entry
@receiver(pre_save, sender=MyModel)
def webhook_sender(sender, ∗∗kwargs):
some_webhook(kwargs["instance"])
如果您正在寻找可以自由使用调试日志的地方,信号处理程序是一个很好的起点。
完成并从项目中移除
作为删除代码前的最后一步,借此机会清理和压缩应用迁移,以便当您从项目中删除代码时,它在没有任何项目引用的情况下开始运行。没有人想进行虚假的迁移。
一旦提取了代码,您就可以将它移出项目。现在还没有必要急着发布 Python 包索引(PyPI)。
从这里,您可以使用 Git 将代码作为子模块包含在您的项目中,或者设置可安装包的基础并包含来自远程源代码库的需求。这允许您开始在多个项目中使用单个代码库。
摘要
在本章中,我们回顾了从项目中移除应用的一些策略,包括如何保留现有的数据库结构,以及当应用代码不再是项目的一部分时,如何启用项目级定制。
在下一章,我们将看看一些将你的应用添加回你的项目的策略。
十一、重新添加你的应用
从项目中提取应用需要几个步骤:
-
重构应用
-
将其移动到顶级名称空间(如果需要)
-
从项目中移除它
第三步给你留下了一个难题,如果你想在你原来的项目中继续使用应用。除非您不需要新的独立应用,或者在某些情况下,您需要在项目中保留原始的、未重构的源代码,否则您需要将新的独立应用包含在衍生它的一个或多个项目中。
本地验证
第一步是使用项目外部本地安装的源代码来验证集成。为此,您可以使用 python setup.py develop 在项目的 Python 环境中进行安装,如第八章所述。注意,如果您的项目只能通过一个虚拟机(例如,使用 vagger)或一个容器(例如,Docker)获得,那么跳到下一个部署步骤会更快。
我们在这一点上的假设是,您已经从项目中移除了应用,无论是从项目根还是从项目存储库中,这样它就不在项目路径中了。至少,这意味着如果您试图在没有以某种明确的方式安装应用的情况下运行您的项目,您可能会遇到一两个导入错误。
然而,简单地从项目根目录之外的应用自己的根目录安装意味着您可以验证项目是否仍然如预期的那样运行,其中的代码已经从项目本身删除。
基于源代码管理的包
从本地安装的包中安装和使用您的应用对于测试和开发新功能来说很好,但在部署项目时就不行了。为此,我们需要远程可用的应用,一个简单的方法是通过源代码控制提供包。作为起点,或者对于没有私有包索引的私有可重用应用,这是一个简单的起点。
Python 包安装程序 pip 允许您以多种方式安装包。当然,您可以只提供包名,pip 将在 Python 包索引中查找指定的包。但是,pip 也可以从远程源代码控制链接安装软件包,也就是 Git 存储库。
完整的细节记录在 pip 网站上,但是简单地说,它通过提供(I)版本控制协议,(ii)到存储库的路径,以及(iii)目标包名来工作。在以下示例中,软件包 myapp 是从 Git repo 安装的,并带有 Git 存储库的完整路径:
pip install git://githost.org/[email protected]#egg=myapp
此示例还通过使用 Git 标记安装了一个特定的版本。片段@v1.0 由两部分组成:@表示后面是存储库中已命名的头,即分支名、标记名或提交 SHA,其余部分是该名称本身。然后,行尾 egg=myapp 指定了目标包的名称。
这可以像包名一样添加到 requirements.txt 文件中。
如果你是从你的 GitHub 库安装的,假设你的用户名是 me,同样的一行应该是这样的:
pip install git://github.com/me/[email protected]#egg=myapp
使用标记或提交的源代码管理版本规范在技术上是可选的,但是为了使用这种策略实际安装包,您应该总是使用通过标记名或提交 SHA 的提交版本。分支名称可能很吸引人,但却是移动的目标,不允许您有效地声明版本。
从 Git 而不是从 Python 包索引(PyPI)使用和安装包的优势主要与发布包的控制和开销有关。您不需要在 PyPI 上注册名称,不需要构建任何东西,也不需要认真担心 changelog 或者您的包元数据是否正确。这些都不是很重要的问题,但是如果你只是想开始重用你的应用,你可以先跳过这些准备工作。
这些限制不仅包括可见性的降低,这是通过 PyPI 和通过一个简短的 pip install myapp 安装提供的,还包括可重用性的降低。可见性对此有影响,但最大的障碍是缺乏连续可用的版本。使用 Git 标签和提交 sha 可以让您锁定特定的版本,这对于单个项目来说是一个很好的策略,但是对于其他包来说却是不可行的,因为您失去了基于范围(例如django>=3.0
)选择版本的能力。当包固定它们需求的版本时,它们将在环境中强制使用那个版本,即使其他包可能需要相同的包。
图 11-1
阴影区域是相互兼容版本的范围
如果两个包都通过提供最小和/或最大支持版本号来要求相同的依赖性(如图 11-1 所示),那么我们通常可以期望找到一些相互兼容的版本。相反,如果这些包中的一个固定了一个特定的版本,,那么它可能会安装这个特定的版本,这个版本不在另一个包指定的兼容范围内,即使这个特定的固定版本是不必要的。
基于源代码控制的包安装排除了像这样的版本范围的使用,并将要求版本锁定。
已发布打包
为了易于使用和安装的一致性,以及在更大的 Django 生态系统中的可见性和访问,您需要将您的应用发布到一个包索引中。发布还允许您充分利用需求规范中的版本号,甚至是私有索引。
发布包的最基本的工作包括(I)在索引上注册包名,(ii)构建包,以及(iii)上传构建文件。
你需要两个额外的包裹,轮子和绳子。wheel 包用于构建 Python wheels,这是预构建的 Python 包,使用。whl 分机。对于开发人员来说,安装 Wheel 文件比安装普通的源代码包要快得多。twine 包用于将您的包构建上传到包索引。您可以在没有 twine 的情况下做到这一点,但 twine 将确保使用 HTTPS,并简化注册和上传多种包格式的步骤。
pip install wheel twine
注册名称是一个一次性的步骤,它会保留包的名称。这可以防止名称冲突,所以您需要确保您选择的包名称还没有被使用。然而,这是我们不会明确采取的一个步骤。可以使用 setup.py 的“注册”命令;然而,这不一定是安全的,twine 会安全地做到这一点,并且不需要添加显式的用户步骤。尽管如此,这是值得注意的一步!
为了利用基于版本号的安装,您的软件包将需要一个版本号。我们希望在两个地方包含版本号,setup.py 文件和您的模块根目录。如果是一个独立的 Django 应用,后者应该是 myapp/init.py。作为传播版本号的更复杂方法的替代,您可以从在两个地方对其进行硬编码开始。
# setup.py
from setuptools import setup, find_packages
setup(
name="blog",
version="0.1.0",
author="Ben Lopatin",
author_email="[email protected]",
url="http://www.django-standalone-apps.com",
packages=find_packages(exclude=["tests"]),
)
# __init__.py
__version__ = "0.1.0"
setup.py 文件中的版本号用于在软件包索引中注册版本号,并在安装时管理版本。您的软件包中包含的版本号,即 init。py 文件是非常有用的元数据,可以用来验证安装和使用的是哪个版本的软件包。这并不意味着它是多余的,但这意味着只改变 init 中的版本。py 文件本身不会对作为新版本发布到包索引的内容产生任何实质性影响。这些更改必须在 setup.py 文件中或通过该文件进行。
- 在第四部分中,我们将会看到一些处理版本号更新的改进方法。
在构建和上传之前,最好快速检查一下您的包元数据是否有效。您可以通过运行 python setup.py check:
python setup.py check -r -s
check 命令将对您的包元数据执行最少的验证。您应该始终运行此步骤,以确保其格式正确。如果检查失败,则-s 选项将导致脚本退出并显示错误代码,而-r 选项检查所有字符串是否都符合 reStructuredText。如果您打算在自述文件中使用 Markdown 并读入 setup.py 文件,则应跳过此选项;否则,这可以防止包索引的格式被破坏。
设置了版本号并验证了元数据后,接下来您需要构建一个发行版,即人们在安装您的应用时实际下载的文件。大致来说,有两种方式来构建这个包:使用一个源发行版和使用一个轮发行版。它们并不相互排斥,所以我们将构建两者(记住,您需要安装 wheel 包来构建包含以下内容的 wheel):
python setup.py sdist bdist_wheel
这将在您的软件包的 dist/目录中创建一个扩展名为. whl 的归档文件和一个归档文件,两者都根据您的软件包名称和版本进行命名(sdist 创建的特定扩展名因系统而异,并且是可配置的)。
然后是上传发行版的时候了,安装了 twine 之后,命令看起来像这样:
twine upload dist/*
如果您还没有注册包名,twine 上传步骤将在上传前首先注册包名。如果成功,您将看到您的新版本——或新软件包——安装在软件包索引上。如果由于某种原因上传失败,例如,只有一个分发选项,您可以修复问题(如果有的话),并尝试重新上传失败的分发。不可能重新上传相同版本的发行版,但是如果发行版没有成功上传,那么这个限制就不适用。
最后一步是标记您的发布版本。使用 Git,您可以使用 tag 命令,就像这样:
git tag -a v0.1.0 -m "Initial version"
标记的目的是确保您可以跟踪每个版本中到底部署了什么。出于这个原因,您应该在成功发布新版本之后的在您的存储库中标记您的提交。如果您必须在上传软件包版本之前进行最终更改,这可以防止标记错误的版本。
摘要
在本章中,您学习了一些将提取的应用作为独立应用添加回项目的策略。您学习了如何在本地完成这项工作,方法是在项目路径中安装应用,使用源代码控制和版本标签远程完成这项工作,最后作为已发布的可安装包发布到 PyPI。我们将在随后的章节中进一步探讨改进打包过程的方法。
十二、处理应用设置
每个 Django 项目都可以通过它的 settings.py 文件的设置模块进行配置。这就是你如何指定你正在使用的数据库和如何连接到它,如何配置你的模板系统,当然还有安装什么应用。一个典型的设置文件包含了 Django 的一般设置(比如数据库和密钥),项目应用设置,当然还有独立应用的设置。
并非每个独立应用都需要自己的用户可配置设置。但是独立应用需要自己的设置有各种各样的原因,包括
- 第三方 API 集成
-
特定于应用的缓存行为
-
功能切换
-
指定依赖关系
-
限制允许的文件类型
-
将设置添加到你自己的项目中非常简单,将它们添加到一个独立的应用中也不是什么难事。但是,由于应用将集成到其他项目中,并且可能包含具有各种值和类型约束的设置,因此需要预先考虑在应用中命名、构建和包含这些内容。
设置命名
首先考虑的是命名。不仅要清楚地命名设置值,还应命名为易于与应用关联的名称。实际上,这意味着它们应该用一个特定于应用的前缀来命名。
可以在 Django 本身的 contrib.auth 应用中找到这样的例子。auth 应用允许您指定自定义用户模型,如下所示:
AUTH_USER_MODEL = "custom_users.User"
这可以用 USER_MODEL 简单而简洁地命名,但是 AUTH _ preamble 确保它显然对应于 AUTH 应用。
因此,如果你的应用公开了一些这样的设置
MAX_API_TIMEOUT = 10
SERVICE_API_KEY = "helloworld123"
确保它们的命名空间与您的应用相对应:
MYAPP_MAX_API_TIMEOUT = 10
MYAPP_SERVICE_API_KEY = "helloworld123"
设置格式
设置最终是可以通过 django 项目中的 django.conf.settings 访问的 Python 对象。因此,尽管我们认为 DEBUG 的设置为布尔值,SECRET_KEY 的设置为字符串值,但它们并不局限于简单类型,甚至不局限于内置类型。DATABASES 设置是一个字典,TEMPLATES 是字典列表,INSTALLED_APPS 和 MIDDLEWARE 是字符串列表。
- 平的比嵌套的好。
在显示应用的配置设置时,显示的值越简单越好。在“什么格式?”根本问题主要是使用多个顶级设置还是一个或多个嵌套设置的字典。
例如,对应用的所有设置值使用一个字典可能很有诱惑力,这样就只有一个“设置”这种方法的好处是保证了最终用户设置文件的简单性,但在许多情况下,它会混淆这些设置的来源。例如,如果最终用户使用 12 因子应用风格运行他们的 Django 项目,并使用环境变量来填充设置值,那么理想情况下,它们应该与顶级设置值具有 1:1 的关系。
- 虽然实用性胜过纯粹性。
这应该是一个很好的默认设置,而不是硬性规定。使用字典公开设置的主要优点是,当设置组相互关联时,它会变得更加明显。在这里的设置片段中,很明显缓存设置是紧密相关的(特别是如果有其他应用设置的话)。
MYAPP_CACHE_TTL = 10
MYAPP_CACHE_KEY_PREFIX = "myapp"
MYAPP_CACHE = {
"TTL": 10,
"KEY_PREFIX": "myapp",
}
然而,使用字典的一个缺点是它可能不太清楚默认值是如何被覆盖的。提供的整个字典是否算作导入的设置?还是使用最终用户的设置来更新现有的默认值?至少当一个顶级 app 设置是而不是添加在最终用户的设置中时,很明显会使用默认。
关于环境变量的最后一点:我应该强调,在 Django 项目中使用环境变量是最终用户的特权,而不是独立的应用开发人员的特权。避免在流程环境中期待值的诱惑,始终依赖设置模块。否则会不必要地限制最终用户提供设置的方式,并且还会强制实施环境变量命名约定,尽管这些约定看起来很合理,但并不适合最终用户自己的情况。
采购应用设置
最后要考虑的是,如何将这些设置放到你的应用中需要的地方。这主要会影响您对应用的使用,因为这些设置通常会在应用内部使用。
下面是 views.py 摘录的一个简短示例,其中几个特定于应用的设置来自 django.conf.settings:
# myapp/views.py
from django.conf import settings
from myapp.client import ApiClient
USE_CACHING = settings.MYAPP_CACHE_SETTINGS["USE_CACHING"]
CACHE_PREFIX = settings.MYAPP_CACHE_SETTINGS["CACHE_PREFIX"]
CACHE_TTL = settings.MYAPP_CACHE_SETTINGS["CACHE_TTL"]
def list_api_resources(request):
client = ApiClient(settings.MYAPP_API_KEY)
api_results = cache.get(f"{CACHE_PREFIX}:results")
if not api_results:
api_results = client.list()
cache.set(f"{CACHE_PREFIX}:results", api_results, CACHE_TTL)
return render(request, "myapp/api_resources.html", {
"api_results": api_results,
})
首先,这里有许多可能出错的地方:
-
设置中可能未定义 MYAPP_CACHE_SETTINGS 名称,或者可能为其分配了错误的类型,从而导致 AttributeError。
-
MY_API_KEY 可能丢失,也会导致 AttributeError。
-
类似地,任何单独的 MYAPP_CACHE_SETTINGS 值都可能丢失,从而导致令人困惑的 KeyError。
-
并且任何单独提供的高速缓存设置可能具有错误的类型,或者错误的值,如果对于 a 设置有合理的值界限。
在您自己的项目中,您可以在设置模块中检查和绑定您的设置值,但这不是您可以委托给应用最终用户的事情。相反,应该检查这些错误,并尽早在您的独立应用中捕获错误。实际上,这意味着检查缺失或格式错误的值,并尽快引发配置错误。
# myapp/views.py
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from myapp.client import ApiClient
if not getattr(settings, "MYAPP_API_KEY"):
raise ImproperlyConfigured("MYAPP_API_KEY must be set")
try:
USE_CACHING = settings.MYAPP_CACHE_SETTINGS["USE_CACHING"]
except (AttributeError, KeyError):
USE_CACHING = False
try:
CACHE_PREFIX = settings.MYAPP_CACHE_SETTINGS["CACHE_PREFIX"]
except (AttributeError, KeyError):
USE_CACHING = "myappp"
try:
CACHE_TTL = int(settings.MYAPP_CACHE_SETTINGS["CACHE_TTL"])
except (AttributeError, KeyError):
CACHE_TTL = 3600
except (TypeError, ValueError):
raise ImproperlyConfigured("MYAPP cache TTL must be a number")
def list_api_resources(request):
""""""
client = ApiClient(settings.MYAPP_API_KEY)
api_results = cache.get(f"{CACHE_PREFIX}:results")
if not api_results:
api_results = client.list()
cache.set(f"{CACHE_PREFIX}:results", api_results, CACHE_TTL)
return render(request, "myapp/api_resources.html", {
"api_results": api_results,
}
现在,至少如果最终用户忘记提供 MYAPP_API_KEY 或者不小心将缓存 TTL 设置为“helloworld ”,您可以用易于理解且有帮助的错误消息来捕捉这些错误。如果缺少一个可能缺少的值,就会提供一个合理的默认值。
然而,包含在具有不同目的的模块中的代码是混乱的,并且如果这些值中的任何一个在其他模块中是必需的,那么要么需要重复这个操作,要么这些其他模块将需要有选择地从 views.py 文件中导入这些清除的值。相反,让我们将所有这些特定于应用的设置移到它们自己的模块中。这将允许您在一个地方封装所有的值检查,并且没有其他模块需要知道这些设置是如何获得或给出的。
虽然 conf.py 和 app_settings.py 也是常见的选择,但这种模块的一个明显的名称是 settings.py。我个人更喜欢 conf.py。第一种是最流行的方法,虽然明智,但这意味着它更容易引起混乱,尤其是在应用中的任何其他模块都导入 django.conf.settings 的情况下;当然,一个解决方案是简单地将那些单独需要的全局设置导入到你的应用设置模块中。
现在有了一个特定于应用的设置模块,可以从中导入这些设置,views.py 和其他模块只需要导入它,就可以避免任何类型的额外错误和默认值处理:
# myapp/conf.py
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# Required values
MYAPP_API_KEY = getattr(settings, "MYAPP_API_KEY")
if not MYAPPP_API_KEY:
raise ImproperlyConfigured("MYAPP_API_KEY is missing")
# Values with defaults
USE_CACHING = True
CACHE_PREFIX = "myapp"
CACHE_TTL = 60 ∗ 60
try:
USE_CACHING = settings.MYAPP_CACHE_SETTINGS["USE_CACHING"]
except (AttributeError, KeyError):
pass
try:
CACHE_PREFIX = settings.MYAPP_CACHE_SETTINGS["CACHE_PREFIX"]
except (AttributeError, KeyError):
pass
try:
CACHE_TTL = int(settings.MYAPP_CACHE_SETTINGS["CACHE_TTL"])
except (AttributeError, KeyError):
pass
except (TypeError, ValueError):
raise ImproperlyConfigured("MYAPP cache TTL must be a number")
摘要
在本章中,我们介绍了处理特定于应用的设置的策略,包括如何命名空间和构建特定于应用的设置,如何在应用中获取这些设置,以及如何最好地处理缺失值和错误值。
在下一章中,我们将看看如何让你的独立应用可以在你自己语言之外的语言中使用。
十三、国际化
国际化和本地化允许不同语言的人使用不同的书写上下文(例如,日期格式)来使用应用。概念上很简单,这是让更多人可以使用软件的一个强有力的方法。
作为一个以英语为母语的国家的人,我认为公平地说,在英语国家,大多数以英语为母语的人很少考虑他们的软件将如何被说其他语言的人或其他国家的人使用。然而,只为一种语言的“市场”编写软件不利于你的利益,因为你用其他语言编写软件的成本相当低,结果是更大的用户群,包括最终用户和潜在贡献者。
有几个步骤可以让你的独立 Django 应用对使用其他语言的人有用,这些步骤同时按顺序和优先级排列。
为什么翻译
举个简单的例子,假设您的应用包含一个执行一些基本验证的表单类。在我们的例子中,它检查优惠券字段中提供的值是否与当前活动的优惠券相匹配。如果不匹配,那么数据就不会被验证,并返回一个错误字符串,表单将显示给用户。
class CouponForm(forms.Form):
coupon = forms.CharField(required=False)
def clean_coupon(self):
data = self.cleaned_data.get('coupon', '')
if data and not Coupon.objects.active().filter(code=data).exists():
raise forms.ValidationError(
"Invalid coupon code"
)
return data
现在,对于您的应用的每个用户,显示的验证消息将始终是“您输入了无效的优惠券代码”,无论他们的网站配置了什么语言。如果您想用西班牙语提供它,那么您需要检查 Django 项目中的字段或特定消息,然后返回一个自定义消息。
class CouponView(FormView):
def form_invalid(self, form):
context = super().get_context_data(form=form)
spanish_errors = {}
if (
form.errors.get("coupon", [""])[0] ==
"Invalid coupon code"
):
spanish_errors["coupon"] = "Cupón inválido"
context["spanish_errors"] = spanish_errors
return self.render_to_response(context)
这显然是一个人为的例子,你可能已经看到了一些简化的方法。但如果这种习俗的改变完全没有必要,那就好了。
稍加修改,它们就没有必要了。
可翻译字符串和翻译如何工作
该解决方案可以完全在 form 类中实现,只需一次导入,并将字符串“包装”在对gettext
的函数调用中:
from django.utils.translation import gettext as _
class CouponForm(forms.Form):
coupon = forms.CharField(required=False)
def clean_coupon(self):
data = self.cleaned_data.get('coupon', '')
if data and not Coupon.objects.active().filter(code=data).exists():
raise forms.ValidationError(
_("Invalid coupon code")
)
return data
我称之为“包装”字符串,因为使用 common _ alias 看起来就是这样,但不要搞错了,这是一个函数调用。执行时,将根据调用上下文中设置的语言环境使用外部程序gettext
获取返回的字符串,该语言环境可以是默认语言环境,也可以是最终用户在会话中选择的语言环境。
以这种方式,国际化只不过是简单的字典查找。然而,与英语-西班牙语词典不同的是,对于所选的单词或短语没有微妙的选项;相反,这种查找的行为更像一个 Python 字典,其中每个字符串都是返回另一个字符串的精确键。
对国际化的一个常见反对意见是,最初的开发人员不知道潜在的语言是什么,或者对它们的了解不足以提供翻译,因此付出努力没有什么意义。幸运的是,让独立的应用可翻译,甚至被翻译,没有这样的要求!
确定翻译步骤的优先顺序
启用翻译的第一步是使字符串可翻译。最简单地说,这意味着如前所述,在对一个gettext
函数的调用中“包装”您的字符串。django.utils.translation 模块中有几个gettext
函数,以及一个模板标记库;它们的详细用法在 Django 的官方文档中有所记载,在此不必赘述。你的应用的首要任务是确保 Python 代码中面向用户的字符串用 gettext
和可翻译的包装。如果你除此之外什么都不做,你就已经完成了关键的 80%。
这是单一优先级的原因有两个:第一,如果字符串可供查找,完全有可能为任何语言创建必要的语言文件;第二,这是最终开发人员唯一不能更改的面向用户的内容。
- 对于面向用户的字符串,我在这里指的是任何期望显示给应用用户并在他们的浏览器中看到的字符串。除非使用异常向最终用户发出消息(例如,通过验证错误),否则您可能不希望翻译异常消息。
使模板可翻译是第二要务,这是第二要务还是第三要务完全取决于 Django 独立应用中模板的性质。这里的原因是模板完全可以被开发者用户扩展和覆盖。如果你的模板是稀疏的,并且完全打算被开发人员替换,那么使这些可翻译的价值是微不足道的。另一方面,如果您的应用中的模板具有丰富的结构,并且旨在成为面向用户的体验的一部分,那么确保这些模板是可翻译的——通过使用来自 i18n 标记库的模板标记——应该是优先考虑的事情。
随着这两项任务的完成,进一步努力的必要性和价值急剧下降,除非您知道特定语言的用例,并准备好创建翻译的资源。这些额外的步骤包括生成和添加 po 文件,即用于gettext
翻译的基于文本的源文件,与翻译服务集成,以及编译和包含 mo 文件,即gettext
使用的二进制查找文件。
生成和添加 po 文件非常简单,完全不需要目标语言知识。然而,它不涉及选择语言!这有点像没有测量就优化。除非你知道对特定语言的需求是什么,否则你无法做出选择。这可能会让你的应用更明显地准备好翻译贡献,但即使这是一个可疑的策略。在西半球使用最广泛的三种语言中,有许多国家特有的变体;而在使用翻译的地方,这些原本很小的差异往往是显著的。
模型内容和翻译
Django 应用中面向用户的内容有几个来源:模板、Python 代码本身以及用户控制的基于模型的内容。在大多数包含大量内容的网站和 web 应用中,基于模型的内容构成了大部分内容。虽然您作为独立的应用作者不提供这些内容,但您可以为开发人员用户提供启示来添加翻译。当然,如何做到这一点,以及这是否必要或有价值,取决于你的独立应用的性质。
在您自己的 Django 项目中,使用您控制的模型,除了下面描述的那些之外,还有几个可用的解决方案。像 django-model translation)这样的第三方独立应用允许您将特定于地区的字段添加到现有模型中,并在最少干预的情况下从您的应用无缝访问这些字段。然而,这涉及到修改数据库表,这意味着数据库迁移,而在第三方应用的情况下,这意味着试图管理不受您控制的库的迁移,而且如果您使用任何类型的临时部署系统,会失去对这些迁移的跟踪,所有这些都意味着对于管理 Django 项目的开发人员用户来说,试图在第三方应用中为模型添加翻译支持是不可行的。谢天谢地,作为 Django 独立应用的开发者,你可以提供一些启示。
对于内容繁重的应用,其中模型有几个或许多表示面向用户内容的字段,一个优秀且灵活的策略是包含一个 locale 字段,并允许翻译随实例而变,或者更具体地说,随数据库行而变。这意味着,例如,对于一个电子邮件模型,您可能允许多个实例具有相同的基础:
from django.conf import settings
from django.db import models
class EmailType:
confirmation = "confirmation"
notification = "notification"
@classmethod
def choices(cls):
return [(cls.notification, cls.notification),
(cls.confirmation, cls.confirmation)]
class EmailMessage(models.Model):
email_type = models.CharField(
max_length=20,
choices=EmailType.choices(),
)
locale = models.CharField(
max_length=10,
choices=settings.LANGUAGES,
default="",
)
message = models.TextField()
class Meta:
unique_together = ("email_type", "locale")
现在有一种内置的方法可以将翻译的内容包含在数据库中,而无需对数据库做任何进一步的修改。不过,这种策略对于“内容密集型”模型来说最有意义,因为这种模型要么代表大量内容,要么代表大量需要一起翻译的字段。
对于只有几个字段需要翻译的模型,另一种选择是利用内置的查找字段,这种选择在撰写本文时还没有被广泛采用。如果您愿意将开发人员用户提交给 PostgreSQL 数据库,那么可以选择使用 HStoreField 或 JSONField。两者都可以用来表示字典;HStoreField 更简单,仅限于字符串,但 JSONField 使用默认的数据库功能(HStoreField 要求您安装数据库扩展)。
对于读者来说,最大限度地发挥这种策略的潜力是一项值得鼓励的工作,但最简单的做法是将核心字段数据存储在字典中:
from django.contrib.postgres.fields import JSONField
from django.db import models
class Product(models.Model):
title_source = JSONField()
price = models.IntegerField()
def title(self, locale=""):
if locale:
try:
return self.title_source[locale]
except KeyError:
pass
return self.title_source[""]
这巧妙地解决了数据存储问题,以及显式检索。这样一个接口的可用性保证了巨大的改进,包括更新数据,特别是像 django-translation 这样的东西所提供的简化查询。也许这是你的第一个 Django 独立应用!
摘要
在这一章中,我们回顾了什么是国际化,以及为什么在你的独立应用中适应国际化很重要。您了解了如何优先为应用添加翻译支持,何时为应用添加特定的语言翻译,以及如何翻译基于模型的内容。
在下一章,我们将学习管理不同 Python 和 Django 版本兼容性的问题以及解决这些问题的一些策略。
十四、管理版本兼容性
当您编写一个 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 版本的限制,用虚线表示。
图 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="[email protected]",
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="[email protected]",
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="[email protected]",
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="[email protected]",
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 = [email protected]
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 <[email protected]>"]
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 应用的体验。
在下一章中,您将了解独立应用的许可,包括软件许可提供的内容以及如何包含它们。
十八、协议
作为开源软件的用户,大多数开发者可能认为我们使用的软件的版权和许可条件是理所当然的。当你发布自己的软件时,这就有点难了。如果您需要在自己的软件中包含其他软件,事情可能会变得更加复杂。
- 作者注:本章中的任何内容都不应被理解为法律建议。如果您对许可或使用许可软件有法律顾虑,您应该寻求专业的法律建议。
许可证有什么作用
许可证做的第一件事是明确声明软件的版权所有。在世界上大多数司法管辖区,仅仅通过创作新作品的行为就授予版权。许多国家提供了注册版权的方法,但这一步不是必需的;版权是自动的。然而,拥有版权和主张版权是两回事。
第二,许可证是一种协议。它们是软件创作者和用户之间的条款协议。在 web 应用的情况下,用户可能是开发人员或部署软件的任何人(例如,企业)。包含的条款多种多样,但协议的机制通常是以这种或那种方式使用软件。
许可证的条款可能很严格,比如那些包含在普通商业许可证中的条款。有些许可证可能禁止复制或重新分发软件、修改软件,甚至禁止以某种方式使用软件。其他许可证,如通用开源许可证,可以自由地允许您对软件做任何您想做的事情,只要您将许可证及其版权和条款包括在内。这些术语是权利和义务的某种组合。
在基本层面上,大多数开源许可证的条款否认使用该软件的任何担保或责任。毕竟,它是免费提供源代码的。这似乎没有必要,尤其是在 Django 独立应用的环境中。然而,鉴于在一个潜在的诉讼社会中,否认与陌生人分享的任何责任的成本接近于零,这是一个不错的选择。
除此之外,如果你的目的是与全世界免费分享你的独立应用,那么似乎没有必要声明任何条款(有专门为此提供的许可证!).然而,许可证的一个关键方面是它明确允许任何人用它做他们想做的事情。这意味着其他人不能为自己主张你的软件,然后规定不同的条款。
许可证的种类
目前使用的许可证有很多种,但它们分为几类:
-
商业许可证
-
开源许可证
-
公共领域许可证
商业许可证是由付费用户颁布的,并且只能由付费用户颁布。例如,微软视窗和苹果 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.
对于较短的许可证,习惯上在这样的注释中重申整个许可证,但是如果你包括了许可证并且使引用变得明显,你不需要这样做。如果您出售的是一个软件包,包括整个软件包,那么您可以依赖模块本身中包含的许可,尽管如此,也可以将原始的许可文件包含到软件包目录中。
摘要
在这一章中,我们回顾了什么是软件许可,它们对您和您的独立应用的用户有什么作用。我们还研究了作为软件作者的您可以使用的许可应用的一些选项、所涉及的权衡、在包含其他软件时如何处理许可,以及在您的独立应用中包含许可信息本身的一些明智策略。
在下一章中,我们将了解如何发布新版本的应用,这一过程中需要什么,以及如何简化它。
十九、记录您的独立应用
一旦你向全世界发布了你的独立应用,其他人就会想要使用它。他们将如何使用它?他们将如何安装它?它能解决他们遇到的同类问题吗?在这一章中,我们将看看如何开始解决记录你的独立应用的挑战。
从问题开始
不管你用什么格式和工具来记录你的独立应用,或者你决定如何在你的代码库中分发文档,你所有的文档都应该以回答别人——甚至是未来的你——在处理你的独立应用时会遇到的问题为动机。
你应该问的几个问题是谁将会阅读这篇文章和他们正在试图做什么。最常见的“谁”是开发者用户,他们要么评估是否使用你的独立应用,要么寻求帮助将其集成到他们的项目中。但也可能是其他人,另一个试图评估你的独立应用是否应该被视为项目选项之一的项目利益相关者。
有人试图做的“什么”更重要,也更容易分解。让我们将这些问题分类如下:
-
这个独立的应用适合我的项目吗?
-
我如何开始使用它?
-
它完全有能力做什么?
-
我如何报告错误或参与进来?
第一个问题是关于考核。我所说的评估并不是指对你的应用进行排名或打分,而是判断某人是否应该使用它。这是人们在查看你的独立应用时会遇到的最广泛、最主观的问题,而且没有单一的、万无一失的方法来解决这个问题。
然而,有几个关键问题你可以也应该解决,这将有助于人们评估你的应用,包括(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 )。
图 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="[email protected]")
@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(
"[email protected]",
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 设置向库提供指向后端类或模块的虚线路径,库将加载并使用该特定的类或模块。这意味着后端是无限可定制的(或几乎如此),允许用户在不同的搜索引擎之间切换,也可以创建适合他们特定需求的修改版本,无论这些需求是小功能还是与一个新的完全不同的支持服务合作。
摘要
在这一章中,你已经了解了各种类型的后端特定的选项,以及如何判断使用它们对于你的独立应用来说是否是一个好的策略。
二十三、协作
开源软件倾向于暗示——尽管它并不必须——与其他开发者合作,通常是来自世界各地的陌生人。我们之前的章节很大程度上是基于这样一个想法,即你将与全世界发布和共享你的源代码。这是如此标准的做法,以至于我们很少考虑为什么我们会这样做,以及可能的收益和成本,更不用说如何实现它们了。
协作可能是一个挑战,但它几乎总是值得的。在这一章中,我们将会看到你如何期望人们合作,你作为维护者的角色,以及一些最大化这些贡献的有效性和最小化开源维护负担的策略。
为什么捐款
从回答“为什么”开始是一个好主意,以允许和促进你的项目中的合作,并理解为什么其他人试图以这样或那样的方式为你的项目做出贡献。
作为维护者,贡献的预期或期望的好处包括:
-
识别 bug
-
更新文档
-
建议功能
-
开发功能
不管是哪种类型的贡献,大多数贡献背后的共同线索都是希望使用你的软件。用户可能会报告一个错误,因为他们只是想帮忙,让软件变得更好(改善一个富有成效的体验),或者因为他们想修复这个问题,这样他们就可以开始或继续使用软件(修复一个阻塞的错误)。推荐某个功能的人可能希望在你的应用中看到该功能,因为他们正在使用或想要使用你的应用,而该功能会进一步改进它(至少对他们来说!).
创作和维护开源项目的类似原因也可以在 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 独立应用。
二十四、使用应用模板
一旦你掌握了创建自己的独立应用的诀窍,你可能会想开始写更多。在这一点上,你可能会发现有很多你不想一遍又一遍地做的决定,还有一些你不想写的必要的样板文件。解决这个问题的一个办法是从模板开始创建应用。
在这一章中,我们将回顾一些从模板创建新的独立应用的选项,包括 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 独立应用提供的提示示例。
图 24-1
py Danny/cookiecutter-django package 提供的提示
有两种方法可以创建您自己的 cookiecutter 项目模板来启动 Django 独立应用:从头开始或者修改现有的项目模板。修改现有的 cookiecutter 非常简单,只需克隆源代码库,进行必要的修改,并使用您的本地克隆作为模板。
cookiecutter path/to/local/cookiecutter
改编一个现有的模板意味着你不必从头开始一切,从计算文件的模板名称到决定如何跟踪各种包的依赖关系。如果你需要的改变太激进,你可以从头开始。从头开始时,请记住,尽管模板有巨大的价值,但模板的核心是将文件从一个源复制到另一个源。换句话说,用尽可能少的配置创建您的结构,只在您需要的时候开始并构建可配置性。
摘要
在这一章中,我们看了从模板创建新的 Django 独立应用的两种方法:使用 Django 的 startapp 命令和通用项目模板工具 Cookiecutter。两者都可以与定制的起始模板一起使用,以加强后续 Django 独立应用的项目设计决策;然而,Cookiecutter 的模板比 startapp 更灵活,应该作为独立的应用模板解决方案优先考虑。