为Django写入第一个补丁

引言¶ T0>

有兴趣回馈社会一点? 也许你已经在Django中发现了一个你希望看到的bug,或者你想添加一个小功能。

贡献给Django本身是看到你自己关心的问题的最好方法。 这看起来可能让人望而生畏,但实际上非常简单。 我们将引导你完成整个过程,所以你可以通过例子来学习。

这个教程是谁的?

也可以看看

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

对于本教程,我们期望您至少对Django的工作原理有一个基本的了解。 这意味着您应该熟悉writing your first Django app的现有教程。 另外,你应该对Python本身有一个很好的理解。 But if you don’t, Dive Into Python is a fantastic (and free) online book for beginning Python programmers.

那些不熟悉版本控制系统和Trac的人会发现本教程及其链接仅包含足够的信息以便开始使用。 不过,如果您计划定期为Django做贡献,您可能需要阅读更多有关这些不同工具的信息。

尽管大多数情况下,本教程试图尽可能地解释,以便它可以用于最广泛的观众。

从哪里得到帮助:

如果您在阅读本教程时遇到困难,请发邮件至django-developers,或者在irc.freenode.net上#django-dev与其他人聊天Django用户谁可能能够帮助。

本教程涵盖什么?

我们将通过第一次为Django提供补丁来帮助您。 到本教程结束时,您应该对所涉及的工具和过程有一个基本的了解。 具体来说,我们将涵盖以下内容:

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

一旦你完成了本教程,你可以查看Django’s documentation on contributing的文档的其余部分。 它包含大量的重要信息,对于任何想成为Django定期贡献者的人来说都是必读的。 如果你有问题,可能会得到答案。

需要Python 3!

当前版本的Django不支持Python 2.7。 Python的下载页面或者使用操作系统的包管理器获取Python 3。

对于Windows用户

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

行为准则

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

安装Git

在本教程中,您将需要安装Git来下载Django的当前开发版本,并为您所做的更改生成补丁文件。

要检查是否安装了Git,请在命令行中输入git 如果您收到消息说这个命令找不到,那么您必须下载并安装它,请参阅Git的下载页面

对于Windows用户

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

如果你对Git不熟悉,你可以通过输入git help来了解更多关于它的命令命令行。

获取Django开发版本的副本

贡献给Django的第一步是获得源代码的副本。 First, fork Django on GitHub. 然后,从命令行使用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 is a useful tool for making this more convenient.

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

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

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

回滚到之前版本的Django

对于本教程,我们将使用ticket #24788作为案例研究,因此我们将在git中将Django的版本历史记录重新映射到应用该故障单的补丁之前。 这将允许我们完成从头开始编写补丁所涉及的所有步骤,包括运行Django的测试套件。

请记住,尽管我们将在下面的教程中使用Django主干的较早版本,但在处理自己的补丁时,您应该始终使用Django的当前开发版本!

注意

这个票的补丁是由PawełMarczewski编写的,它被应用到Django中,作为commit 4df7e8483b2679fc1cba3410f08960bac6f51115 Consequently, we’ll be using the revision of Django just prior to that, commit 4ccfc4439a7add24f8db4ef3960d02ef8ae09887.

导航到Django的根目录(包含djangodocstestsAUTHORS等) 。 然后,您可以查看下面教程中将要使用的Django的旧版本:

$ git checkout 4ccfc4439a7add24f8db4ef3960d02ef8ae09887

第一次运行Django的测试套件

在为Django做贡献时,代码更改不会将错误引入到Django的其他区域是非常重要的。 检查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 indicates that an error was raised during a test, and F indicates that a test’s assertions failed. 这两个都被认为是测试失败。 同时,xs分别表示预期的失败和跳过的测试。 圆点表示通过测试。

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

一旦测试完成,您将收到一条消息,通知您测试套件是否通过。 由于您还没有对Django代码进行任何更改,所以整个测试套件都应该通过。 如果您遇到失败或错误,请确保您已正确执行了上述所有步骤。 有关更多信息,请参阅Running the unit tests 如果您使用的是Python 3.5+,那么会出现一些与您可以忽略的弃用警告相关的故障。 Django已经修复了这些失败。

请注意,最新的Django中继可能并不总是稳定的。 在针对主干进行开发时,您可以检查Django的持续集成构建以确定故障是否特定于您的机器,或者它们是否也存在于Django的官方版本中。 如果您单击查看特定版本,则可以查看“配置矩阵”,其中显示了由Python版本和数据库后端分解的故障。

注意

For this tutorial and the ticket we’re working on, testing against SQLite is sufficient, however, it’s possible (and sometimes necessary) to run the tests using a different database.

为你的补丁创建一个分支

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

$ git checkout -b ticket_24788

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

为您的票据写一些测试

在大多数情况下,要将补丁纳入Django,必须包含测试。 对于错误修复补丁,这意味着编写一个回归测试来确保这个错误不会在稍后重新引入到Django中。 应该写一个回归测试,以便在错误仍然存​​在的情况下将会失败,并且一旦错误得到修复就会通过回归测试。 对于包含新功能的修补程序,您需要包含确保新功能正常工作的测试。 当新特征不存在时,它们也应该失败,并且一旦实施就通过。

一个很好的方法是在对代码进行任何更改之前先编写新的测试。 This style of development is called test-driven development and can be applied to both entire projects and single patches. 在编写测试之后,你再运行它们来确保它们确实失败(因为你还没有修复那个bug或者添加了这个功能)。 如果你的新测试没有失败,你需要修复它们,以便他们这样做。 毕竟,无论是否存在错误,通过的回归测试对于防止该错误再次出现并不是很有帮助。

现在以我们的实践为例。

写一些测试#24788

Ticket #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参数仍然可行。

但是这个测试的东西看起来挺难...

如果你以前从来没有处理过测试,乍一看可能会看起来有些困难。 Fortunately, testing is a very big subject in computer programming, so there’s lots of information out there:

  • 编写Django测试的第一步可以在Writing and running tests的文档中找到。
  • Dive Into Python (a free online book for beginning Python developers) includes a great introduction to Unit Testing.
  • 阅读完这些之后,如果你想要一些有趣的东西来吸引你的注意力,总会有Python unittest文档。

运行你的新测试

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

$ ./runtests.py forms_tests

如果测试正确运行,您应该看到与我们添加的测试方法相对应的一个失败。 如果所有测试都通过了,那么您需要确保将上面显示的新测试添加到适当的文件夹和类中。

为您的票据编写代码

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

编写票#24788 的代码

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

class BaseForm:
    # 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,但它有助于识别许多错误和回归,否则这些错误和回归可能会被忽视。

要运行整个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:
     # 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:
         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的claiming tickets and submitting patches的文档。 它涵盖了Trac礼仪,如何为自己申请门票,预期的补丁编码风格,以及许多其他重要细节。
  • 首次贡献者还应该阅读Django的documentation for first time contributors 对于我们这些新手帮助Django的人来说,它有很多好的建议。
  • 在这之后,如果您仍然渴望了解更多关于贡献的信息,您可以随时浏览Django’s documentation on contributing的文档的其余部分。 它包含大量有用的信息,应该是您回答任何问题的第一手资料。

找到你的第一张真正的票

一旦你浏览了一些信息,你就会准备出去找一张自己的门票来写一个补丁。 要特别注意“轻松拣货”标准的门票。 这些门票往往简单得多,对于初次参与者来说非常棒。 一旦你熟悉了对Django的贡献,你可以继续编写更复杂,更复杂的门票。

如果你只是想开始(没有人会责怪你!),请尝试查看需要修补程序的简单故障单具有需要改进的修补程序的简单故障单的列表。 如果您对编写测试非常熟悉,您还可以查看需要测试的简单票的列表。 请记住遵循有关声明门票的指导原则,这些指导是在链接到Django的claiming tickets and submitting patches文档的链接中提到的。

创建一个pull请求后,下一步是什么?

一张票有补丁后,需要第二套眼睛审查。 在提交拉取请求之后,通过设置标签上的标志来表示“有补丁”,“不需要测试”等来更新标签元数据,以便其他人可以找到它进行审查。 贡献并不一定意味着从头开始编写补丁。 审查现有的补丁也是一个非常有用的贡献。 有关详细信息,请参阅Triaging tickets