编写Django的第一个补丁

引言¶ T0>

有兴趣为社区做出点贡献吗? 也许你会在Django中发现你想要修复的漏洞,或者你希望为它添加一个小功能。

为Django作贡献这件事本身就是使你的顾虑得到解决的最好方式。 一开始这可能会使你怯步,但事实上是很简单的。 整个过程中我们会一步一步为你解说,所以你可以通过例子学习。

这个教程是给谁的?

请参见

如果您正在寻找有关如何提交修补程序的参考,请参阅Submitting patches文档。

使用教程前,我们希望你至少对于Django的运行方式有基础的了解。 这意味着你可以自如地在编写你的第一个Django应用程序时使用教程。 除此之外,你应该对于Python本身有很好的了解。 如果您并不太了解, 我们为您推荐Dive Into Python,对于初次使用Python的程序员来说这是一本很棒(而且免费)的在线电子书。

对于版本控制系统及Trac不熟悉的人来说,这份教程及其中的链接所包含的信息足以满足你们开始学习的需求。 然而,如果你希望定期为Django贡献,你可能会希望阅读更多关于这些不同工具的信息。

当然对于其中的大部分内容,Django会尽可能做出解释以帮助广大的读者。

获得帮助:

如果你在使用本教程时遇到困难,你可以发送信息给django-developers 或者登陆 #django-dev on irc.freenode.net 向其他Django使用者需求帮助。

本教程涵盖什么?

一开始我们会帮助你为Django编写补丁, 在教程结束时,你将具备对于工具和所包含过程的基本了解。 准确来说,我们的教程将包含以下几点:

  • 安装Git。
  • 如何下载Django的开发副本
  • 运行Django的测试组件
  • 为你的补丁编写一个测试
  • 为你的补丁编码。
  • 测试你的补丁。
  • 提交拉请求。
  • 去哪里寻找更多的信息。

一旦你完成了这份教程,你可以浏览剩下的Django’s documentation on contributing. 它包含了大量信息。任何想成为Django的正式贡献者必须去阅读它。 如果你有问题,它也许会给你答案

Python 3需要!

本教程假定您使用的是Python 3。 Python的下载页面或您的操作系统的软件包管理器获取最新版本。

对于Windows用户

在Windows上安装Python时,请确保选中“将python.exe添加到路径”选项,以便在命令行中始终可用。

行为准则

作为贡献者,您可以帮助我们保持Django社区的开放和包容。 请阅读并遵守我们的行为准则

安装Git

使用教程前,你需要安装好Git,下载Django的最新开发版本并且为你作出的改变生成补丁文件

为了确认你是否已经安装了Git, 输入 git 进入命令行。 如果信息提示命令无法找到, 你就需要下载并安装Git, 详情阅读 Git’s download page.

对于Windows用户

在Windows上安装Git时,建议您选择“Git Bash”选项,以便Git在自己的shell中运行。 本教程假定您已安装它。

如果你还不熟悉 Git, 你可以在命令行下输入 git help 了解更多关于它的命令(确认已安装)。

获取Django开发版本的副本

为Django贡献的第一步就是获取源代码复本。 首先,在gitHub上的fork Django 然后,从命令行,使用cd命令导航到您想要Django的本地副本的目录。

使用下面的命令来下载Django的源码库

$ git clone git@github.com:YourGitHubName/django.git

现在你有一个Django的本地副本,你可以安装它,就像你将使用pip安装任何包。 最方便的方法是使用一个虚拟环境(或virtualenv),这是Python内置的功能,允许您为每个项目保留已安装软件包的单独目录,以便它们不要互相干扰。

将所有的virtualenvs保存在一个地方是一个好主意,例如您的主目录中的.virtualenvs/ 创建它如果还不存在:

$ mkdir ~/.virtualenvs

现在通过运行以下方法创建一个新的virtualenv:

$ python3 -m venv ~/.virtualenvs/djangodev

路径是新的环境将保存在您的计算机上。

对于Windows用户

如果您还在Windows上使用Git Bash shell,则使用内置的venv模块将不起作用,因为激活脚本仅为系统shell创建(.bat)和PowerShell(.ps1)。 改用virtualenv包:

$ pip install virtualenv
$ virtualenv ~/.virtualenvs/djangodev

对于Ubuntu用户

在某些版本的Ubuntu上,上述命令可能会失败。 使用virtualenv包,首先确保你有pip3

$ sudo apt-get install python3-pip
$ # Prefix the next command with sudo if it gives a permission denied error
$ pip3 install virtualenv
$ virtualenv --python=`which python3` ~/.virtualenvs/djangodev

设置virtualenv的最后一步是激活它:

$ source ~/.virtualenvs/djangodev/bin/activate

如果source命令不可用,则可以尝试使用一个点:

$ . ~/.virtualenvs/djangodev/bin/activate

对于Windows用户

要在Windows上激活您的virtualenv,请运行:

$ source ~/virtualenvs/djangodev/Scripts/activate

每当打开一个新的终端窗口时,都必须激活virtualenv。 virtualenvwrapper是一个有用的工具,使其更方便。

从现在开始,您通过pip安装的任何内容都将安装在新的virtualenv中,与其他环境和系统级软件包隔离。 此外,当前激活的virtualenv的名称显示在命令行上,以帮助您跟踪正在使用哪一个。 继续安装以前克隆的Django副本:

$ pip install -e /path/to/your/local/clone/django/

Django的安装版本现在指向您的本地副本。 您将立即看到您所做的任何更改,这在编写第一个补丁时非常有帮助。

回到Django 之前的版本

对于本教程,我们将使用票证#24788作为案例研究,因此我们将在git之前将Django的版本历史倒转到该票证的修补程序被应用之前。 这样的话我们就可以参与到从草稿到补丁的所有过程,包括运行Django的测试套件。

请记住,为了下面的教程,我们将使用Django的主干旧版本,当您使用自己的补丁程序时,应始终使用Django的当前开发版本!

这张票的补丁是由PawełMarczewski编写的,它被应用于Django作为commit 4df7e8483b2679fc1cba3410f08960bac6f51115 因此,我们将在之前使用Django的修订版,commit 4ccfc4439a7add24f8db4ef3960d02ef8ae09887

首先打开Django源码的根目录(这个目录包含了 tests, django, AUTHORS, docs, 等) 然后你你可以根据下面的教程check out老版本的Django:

$ git checkout 4ccfc4439a7add24f8db4ef3960d02ef8ae09887

第一次运行Django的测试套件

当你贡献代码给Django的时候,一个非常重要的问题就是你修改的代码不要给其他部分引入新的bug。 有个办法可以在你更改代码之后检查Django是否能正常工作,就是运行Django的测试套件。 如果所有的测试用例都通过,你就有理由相信你的改动完全没有破坏Django。 如果你从来没有运行过Django的测试套件,那么比较好的做法是事先运行一遍,熟悉下正常情况下应该输出什么结果。

运行测试套件之前,先将cd -ing安装到Django tests/目录中,然后运行:

$ pip install -r requirements/py3.txt

如果在安装期间遇到错误,则您的系统可能缺少一个或多个Python包的依赖关系。 请参阅失败的软件包文档或使用您遇到的错误消息搜索Web。

现在我们准备运行测试套件。 如果您使用GNU / Linux,macOS或其他一些风格的Unix,请运行:

$ ./runtests.py

现在坐下来放松一下。 Django的整个测试套件有超过9600种不同的测试,因此可能需要5到15分钟的时间才能运行,具体取决于您的计算机的速度。

Django的测试套件运行时,您将看到一个字符流代表每个测试的运行的状态。 E 表示测试中出现异常 和 F 表示断言失败。 这两种情况都被认为测试失败。 同时,xs 分别表示与期望结果不同和跳过测试。 点表示测试通过。

跳过的测试通常是由于缺少运行测试所需的外部库;请参阅Running all the tests以获得依赖关系列表,并确保安装任何与您正在进行的更改相关的测试(本教程不需要任何内容​​)。 一些测试特定于特定的数据库后端,如果没有使用该后端测试,则会跳过此测试。 SQLite是默认设置的数据库后端。 要使用其他后端运行测试,请参阅Using another settings module

当测试执行完毕后,得到反馈信息显示测试已通过,或者测试失败。 因为还没有对 Django 的源码做任何修改,所有的测试用例应该测试通过。 如果测试失败或出现错误,回头确认以上执行操作是否正确。 查看 Running the unit tests 获取更多信息。 如果您使用的是Python 3.5+,则会出现与您可以忽略的弃用警告相关的几个故障。 这些失败已经在Django中被修复了。

注意最新版本 Django 分支不总稳定。 当在分支上开发时,你可以查看代码持续集成构建页面的信息 Django’s continuous integration builds 来判断测试错误只在你指定的电脑上发生,还是官方版本中也存在该错误。 如果点击某个构建信息,可以通过配置列表信息查看错误发生时 Python 以及后端数据库的信息。

在本教程以及所用分支中,测试使用数据库 SQLite 即可, 然而在某些情况下需要 run the tests using a different database

为你的补丁创建一个分支

在进行任何更改之前,请为该机票创建一个新的分支:

$ git checkout -b ticket_24788

您可以为分支选择任何名称,“ticket_24788”就是一个例子。 该分支中所做的所有更改将特定于故障单,不会影响我们之前克隆的代码的主要副本。

为您的ticket编写一些测试

大多数情况下,Django 的补丁必需包含测试。 Bug 修复补丁的测试是一个回归测试,确保该 Bug 不会再次在 Django 中出现。 该测试应该在 Bug 存在时测试失败,在 Bug 已经修复后通过测试。 新功能补丁的测试必须验证新功能是否正常运行。 新功能的测试将在功能正常时通过测试,功能未执行时测试失败。

最好的方式是在修改代码之前写测试单元代码。 这种开发风格叫做 测试驱动开发 被应用在项目开发和单一补丁开发过程中。 测试单元编写完毕后,执行测试单元,此时测试失败(因为目前还没有修复 BuG 或 添加新功能), 如果测试成功通过,你需要重新修改测试单元保证测试失败。 然而测试单元并没有阻止 BUG 发生的作用。

现在我们的操作示例。

对tickets进行一些测试

#24788提出了一个小功能添加:能够在Form类上指定类级别属性prefix,以便:

[…] forms which ship with apps could effectively namespace themselves such
that N overlapping form fields could be POSTed at once and resolved to the
correct form.

为了解决这张票,我们将在BaseForm类中添加一个prefix属性。 当创建此类的实例时,将前缀传递给__init__()方法仍将在创建的实例上设置该前缀。 但不传递前缀(或传递None)将使用类级别的前缀。 在更改代码之前,我们需要一组测试来验证将添加的功能现在以及未来都能正常工作。

导航到Django的tests/forms_tests/tests/文件夹并打开test_forms.py文件。 test_forms_with_null_boolean函数之前的1674行上添加以下代码:

def test_class_prefix(self):
    # Prefix can be also specified at the class level.
    class Person(Form):
        first_name = CharField()
        prefix = 'foo'

    p = Person()
    self.assertEqual(p.prefix, 'foo')

    p = Person(prefix='bar')
    self.assertEqual(p.prefix, 'bar')

此新测试检查设置类级别前缀是否符合预期,并且在创建实例时传递prefix参数仍然有效。

但这个测试的东西看起来有点难吗

如果你没有写过测试,第一眼看上去测试代码会有点难。 幸运的是测试在编程里是一个 非常 重要的部分, 因此下面有更多的相关信息:

  • 为 Django 添加测试代码浏览官网文档: Writing and running tests.
  • 深入 Python (一个在线免费的 Python 初学者教程) 包含了非常棒的 测试单元介绍.
  • 看完这些之后,如果你想要一些更小的东西把你的牙齿吸入,总是有Python unittest文档。

运行新的测试

记住,我们还没有对BaseForm进行任何修改,所以我们的测试将会失败。 我们来运行forms_tests文件夹中的所有测试,以确保真的发生了什么。 在命令行中 cd Django 的 tests/ 目录并执行:

$ ./runtests.py forms_tests

如果测试运行正常,则应该看到与我们添加的测试方法相对应的一个故障。 如果所有测试方法都正常通过,请检查上面的测试方法是否添加到了正确的文件位置。

为你的ticket写一些代码

接下来,我们将把Ticket #24788中描述的功能添加到Django。

为ticket #24788写代码

导航到django/django/forms/文件夹,然后打开forms.py文件。 在第72行找到BaseForm类,并在field_order属性之后添加prefix类属性:

class BaseForm(object):
    # This is the main implementation of all the Form logic. Note that this
    # class is different than Form. See the comments by the Form class for
    # more information. Any improvements to the form API should be made to
    # *this* class, not to the Form class.
    field_order = None
    prefix = None

验证您的测试现在通过

修改 Django 源码后,我们通过之前编写的测试方法来验证源码修改是否工作正常。 要将forms_tests文件夹cd中的测试运行到Django tests/目录中,然后运行:

$ ./runtests.py forms_tests

哦,好事是我们写了这些测试! 您仍然会看到一个失败,但出现以下异常:

AssertionError: None != 'foo'

我们忘了在__init__方法中添加条件语句。 Go ahead and change self.prefix = prefix that is now on line 87 of django/forms/forms.py, adding a conditional statement:

if prefix is not None:
    self.prefix = prefix

重新运行测试方法正常会通过测试。 如果没有,请确保您正确地修改了BaseForm类,并正确复制了新的测试。

第二次运行Django的测试套件

如果已经确认补丁以及测试结果都正常,现在是时候运行 Django 完整的测试用例,验证你的修改是否对 Django 的其他部分造成新的 Bug。 虽然测试用例帮助识别容易被人忽略的错误,但测试通过并不能保证完全没有 Bug 存在。

运行 Django 完整的测试用例, cd Django 下 tests/ 目录并执行:

$ ./runtests.py

只要没有看到测试异常,你可以继续下一步骤。

写文件

这个新功能信息应该被记录到文档。 django/docs/ref/forms/api.txt的行1068(文件末尾)添加以下部分:

The prefix can also be specified on the form class::

    >>> class PersonForm(forms.Form):
    ...     ...
    ...     prefix = 'person'

.. versionadded:: 1.9

    The ability to specify ``prefix`` on the form class was added.

由于这个新功能将在即将发布的版本中,它也被添加到文件docs/releases/1.9.txt中的“Forms”部分的第164行Django 1.9的发行说明中:

* A form prefix can be specified inside a form class, not only when
  instantiating a form. See :ref:`form-prefix` for details.

关于 versionadded 的解释以及文档编写的更多信息,请参考 Writing documentation 这个页面还介绍了怎么在本地重新生成一份文档,你可以查看新生成的 HTML 文档页面.

预览您的更改

现在是时候通过我们的补丁所做的所有改变。 要显示您当前的Django副本(与您的更改)之间的差异,以及您最初在本教程中检出的修订版本:

$ git diff

使用箭头键上下移动。

diff --git a/django/forms/forms.py b/django/forms/forms.py
index 509709f..d1370de 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -75,6 +75,7 @@ class BaseForm(object):
     # information. Any improvements to the form API should be made to *this*
     # class, not to the Form class.
     field_order = None
+    prefix = None

     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                  initial=None, error_class=ErrorList, label_suffix=None,
@@ -83,7 +84,8 @@ class BaseForm(object):
         self.data = data or {}
         self.files = files or {}
         self.auto_id = auto_id
-        self.prefix = prefix
+        if prefix is not None:
+            self.prefix = prefix
         self.initial = initial or {}
         self.error_class = error_class
         # Translators: This is the default suffix added to form field labels
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 3bc39cd..008170d 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -1065,3 +1065,13 @@ You can put several Django forms inside one ``<form>`` tag. To give each
     >>> print(father.as_ul())
     <li><label for="id_father-first_name">First name:</label> <input type="text" name="father-first_name" id="id_father-first_name" /></li>
     <li><label for="id_father-last_name">Last name:</label> <input type="text" name="father-last_name" id="id_father-last_name" /></li>
+
+The prefix can also be specified on the form class::
+
+    >>> class PersonForm(forms.Form):
+    ...     ...
+    ...     prefix = 'person'
+
+.. versionadded:: 1.9
+
+    The ability to specify ``prefix`` on the form class was added.
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 5b58f79..f9bb9de 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -161,6 +161,9 @@ Forms
   :attr:`~django.forms.Form.field_order` attribute, the ``field_order``
   constructor argument , or the :meth:`~django.forms.Form.order_fields` method.

+* A form prefix can be specified inside a form class, not only when
+  instantiating a form. See :ref:`form-prefix` for details.
+
 Generic Views
 ^^^^^^^^^^^^^

diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 690f205..e07fae2 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -1671,6 +1671,18 @@ class FormsTestCase(SimpleTestCase):
         self.assertEqual(p.cleaned_data['last_name'], 'Lennon')
         self.assertEqual(p.cleaned_data['birthday'], datetime.date(1940, 10, 9))

+    def test_class_prefix(self):
+        # Prefix can be also specified at the class level.
+        class Person(Form):
+            first_name = CharField()
+            prefix = 'foo'
+
+        p = Person()
+        self.assertEqual(p.prefix, 'foo')
+
+        p = Person(prefix='bar')
+        self.assertEqual(p.prefix, 'bar')
+
     def test_forms_with_null_boolean(self):
         # NullBooleanField is a bit of a special case because its presentation (widget)
         # is different than its data. This is handled transparently, though.

完成预览补丁后,按q键返回命令行。 如果补丁的内容看起来不错,现在是提交更改的时候了。

提交补丁中的更改

提交更改:

$ git commit -a

这将打开一个文本编辑器来键入提交消息。 按照commit message guidelines,并写下如下消息:

Fixed #24788 -- Allowed Forms to specify a prefix at the class level.

推送提交和提出请求

提交修补程序后,将其发送到GitHub上的分支(如果不同,则将其替换为“ticket_24788”)。

$ git push origin ticket_24788

您可以通过访问Django GitHub页面来创建提取请求。 您将在“您最近推荐的分行”下看到您的分支。 点击旁边的“比较和拉动请求”。

请不要为此教程,但在下一页显示修补程序的预览,您将单击“创建拉请求”。

恭喜,您已经学会了如何向Django提出请求! 您可能需要的更多高级技术的细节在Working with Git and GitHub

现在,您可以通过帮助改进Django的代码库来使这些技能很好用。

有关新贡献者的更多信息

在你开始为 Django 编写补丁时,这里有些信息,你应该看一看:

找到你的第一张真实票

一旦你看过了之前那些信息,你便已经具备了走出困境,为自己编写补丁寻找门票的能力。 对于那些有着“容易获得”标准的门票要尤其注意。 这些门票实际上常常很简单而且对于第一次撰写补丁的人很有帮助。 一旦你熟悉了给Django写补丁,你就可以进一步为更难且更复杂的门票写补丁。

如果你只想开始上手(没有人会怪你!),请尝试查看需要补丁的简单票据列表和需要改进的补丁的简单票据 如果你比较擅长写测试,那么你也可以看看这个 需要测试的简单标签列表. 一定要记得遵循在Django的文档claiming tickets and submitting patches中提到的关于声明标签的指导规则.

创建拉动请求后的下一步?

一旦一个标签有了补丁,那么它就需要其他人来重审。 提交拉动请求后,通过设置机票上的标志来更新机票元数据,以说“有补丁”,“不需要测试”等,以便其他人可以找到它进行审查。 从零开始写补丁并不是做贡献的唯一方式。 重审一些已经存在的补丁也是一种非常有用的做贡献方式。 点击Triaging tickets 查看更多详细信息.