为 Django 编写你的第一个补丁

    回馈 Django 本身就是看到自己所关心的问题的最佳方式。也许初看会吓到你,但这实际上是很简单的。我们将带你走一遍整个流程,好让你可以从例子中学习。

    本教程的目标对象?

    在本教程里,我们期望你至少能基本明白 Django 是如何工作的。这意味着你应该顺利地读完了。还有,你应该对 Python 本身也有了很好地了解。如果没有,可以在线看看这本有趣的 Dive Into Python,它面向 Python 新手程序员。

    不熟悉版本控制系统和 Trac 的人会发现本教程及其链接的信息刚刚好够开始。但是,如果你打算定期为 Django 做贡献,你可能会想了解更多关于这些不同工具的知识。

    在大多数情况下,本教程会尽可能多地进行说明,以确保受众能接受。

    哪里可以获得帮助:

    如果你在阅读或实践本教程中遇到困难,请发消息给 或加入IRC频道 django on irc.freenode.net 来与其他 Django 用户进行交流,他们也许能帮到你。

    本教程覆盖了哪些知识点

    首先我们会带你走完为 Django 贡献补丁的流程。在本教程结束时,你应该对工具和流程都有了基本认识。特别地,我们将会介绍以下内容:

    • 安装 Git
    • 如何下载 Django 开发版的拷贝
    • 运行 Django 的测试套件
    • 为你的补丁编写测试程序
    • 为你的补丁编写代码
    • 测试你的补丁
    • 提交拉去请求(pull request)
    • 哪里可以查看更多信息

    一旦你完成了本教程,你就可以去看完的剩下部分了。那里有很多信息,而且是那些想成为定期贡献者必看的。如果你有问题,你也可能在那得到答案。

    需要 Python 3

    本教程假定你用的就是 Python 3。请到Python 官网下载页或者你系统的包管理安装最新版的 Python3。

    给 Windows 用户

    给 Windows 安装 Python 时,请确保把 python.exe 添加进了 Path,这样可以直接在命令行使用。

    行为准则(Code of Conduct)

    作为一名贡献者,你可以帮助我们保持 Django 社区的开放和包容。请阅读和遵循我们的

    安装 Git

    本教程里,为了下载到最新的开发版 Django 和生成你修改过的补丁文件,你将需要安装 Git。

    为了确认是否安装了 Git,你可以在命令行输入 git。如果提示说命令找不到,那你就需要下载安装了,Git 下载页

    如果你对 Git 不太熟悉,你可以在命令行输入 git help 获取更多命令信息。

    下载 Django 开发版的拷贝

    第一步就是得到 Django 源码的拷贝。首先。然后在命令行里,使用 cd 切换到你想放 Django 本地拷贝的目录里。

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

    现在你有了一份 Django 的本地拷贝,你可以安装它,就像使用 pip 安装其他包那样。最方便的方式是使用 虚拟环境(virtual environment)(或者 virtualenv),这是 Python 内置的功能,允许您为每个项目单独设立已安装软件包的目录,让它们不会相互干扰。

    最好是把你所以的虚拟环境(virtualenvs)都放在一个目录下,比如在你的 home 目录下的 .virtualenvs/ 目录。如果还没创建:

    现在,运行以下命令创建新的虚拟环境(virtualenvs):

    1. $ python3 -m venv ~/.virtualenvs/djangodev

    路径就是新的虚拟环境,而它会被保存在你的电脑里。

    给 Windows 用户

    如果你在 Windows 下使用 Git 的 shell,使用内置的 venv 模块会无效,由于启动脚本是为系统 shell 创建的(.bat)和 PowerShell (.ps1)。使用 virtualenv 包代替 venv 模块:

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

    给 Ubuntu 用户

    在一些 Ubuntu 版本中,上面的命令可能会失败。

    1. $ sudo apt-get install python3-pip
    2. # 如果下面的命令因为没有权限出错了,那就加上 sudo
    3. $ pip3 install virtualenv
    4. $ virtualenv --python=`which python3` ~/.virtualenvs/djangodev

    最后一步就是让你的虚拟环境(virtualenvs)生效:

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

    如果 source 命令无效,你可以试试用一个 “点” 代替:

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

    给 Windows 用户

    为了在 Windows 上让虚拟环境(virtualenvs)生效,运行下面命令:

    1. $ source ~/virtualenvs/djangodev/Scripts/activate

    无论什么时候,当你打开一个新的终端窗口时都要激活一下虚拟环境(virtualenvs)。为了方便这种情况,virtualenvwrapper 是很有用的工具。

    从现在开始,你用 pip 安装的任何东西都会被安装进你新的的虚拟环境(virtualenvs),这会隔绝其他环境和系统包。同样,当前激活的虚拟环境(virtualenvs)的名字会在命令行前面显示,这可以让你知道使用的是哪个虚拟环境。

    继续,安装之前克隆(clone)下来的 Django 拷贝:

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

    安装的 Django 现在指向了你的本地拷贝。你将马上看到你做的任何修改,这对你编写第一个补丁很有帮助。

    本教程里,我们将会使用 任务(ticket)作为例子学习,所以在任务(ticket)补丁应用之前,我们将回退到 Django 的历史版本。这让我们可以走完所有步骤,包括从头开始写编写补丁,还有运行 Django 的测试套件。

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

    切换到 Django 的根目录(那里有 django,docs,tests,AUTHORS 等),你可以检查下这个旧版本 Django:

    第一次运行 Django 的测试套件

    这非常重要:当向 Django 贡献时,你修改的代码不会将 bug 引到 Django 的其他地方。一个检查 Django 还可以工作的方法就是在你修改之后运行 Django 的测试套件。如果所有的测试都通过了,你就有理由确认你的修改不会完全破坏 Django。如果你之前没有运行过测试套件,最好在这之前运行熟悉下它的输出大概是什么样的。

    在运行之前,cd 切换到 Django 的 tests/ 目录,安装它的依赖关系:

    1. $ pip install -r requirements/py3.txt

    如果你在安装过程中遇到错误,你的系统可能有一些 Python 包的依赖关系缺失。查询一下安装失败的包的文档,或者在网上搜索一下你遇到的这个错误信息。

    现在我们准备好运行测试套件了。如果你用的是 GNU/Linux,macOS 或者其他 Unix 风格的系统,运行:

    1. $ ./runtests.py

    现在坐下来歇会。Django 整个测试套件有超过 9,600 个不同的测试,所以需要 5 到 15 分钟的时间运行,这取决于你电脑的运算速度。

    当 Django 测试套件运行的时候,你可以看到一段显示着每个正在运行的测试的状态的字符流。E 表示在测试期间抛出了一个错误,F 表示一个测试的断言(assertions)失败。这两者都可以被认为测试失败了。同时,xs 分别表示预期失败和跳过测试。“点” 表示通过测试。

    跳过测试是由于运行测试时缺少额外的库;可以查看运行所有测试所需要的一系列的依赖,确认和你修改的相关的一些测试依赖被安装了(本教程我们什么都不需要安装)。一些测试是特别针对特别的后台数据库的,如果不对那后台进行测试,就会被跳过。SQLite 是默认设置的后台数据库。如果是使用其他的后台数据库,为了运行测试,可以查看。

    一旦测试完毕,你应该会收到一条关于测试套件是通过了还是失败了的信息。由于你还没有对 Django 的代码做修改,所以整个测试套件 应该是(should) 通过。如果你得到的是失败或者错误的信息,请确保你之前走的所有步骤都正确。可以查看运行单元测试获得更多信息。如果你正在使用 Python3.5+,将会有些与不被赞成的警告(deprecation warnings)相关的失败,那些你可以忽略掉。这些失败已经被 Django 修复了。

    注意,最新的 Django 主干(trunk)版本不总是稳定的。当开发版本遇上主干(trunk)版本,你可以检查 来确认是特别针对你的机器的失败还是已经存在在 Django 官方构建里的。如果你点击去看一个特别的构建,你可以看到 “配置矩阵(Configuration Matrix)” 显示着由于 Python 版本和数据库后台问题出现的失败。

    注意

    在本教程和我们正在工作的任务(ticket)里,测试使用 SQLite 足够了,但是,运行其他数据库进行测试是可能的(有时是必要的)。

    为你的补丁创建一个分支

    在修改之前,先为任务(ticket)创建一个新的分支:

    1. $ git checkout -b ticket_24788

    你可以为这个分支选择任何你想要的名字,例如 “ticket_24788”。在这个分支做的所有修改都是特别针对这个任务的,并不会影响到我们先前克隆下来的主要代码。

    为你的任务编写一些测试

    在大多数情况下,被 Django 接收进来的补丁都必须测试过。针对 bug 修复的补丁,这意味着要编写回退测试代码来确保 bug 不会在 Django 的后续版本再次出现。一个回退测试应该是这样的:当 bug 存在时会失败,当 bug 被修复时会通过。对于包含新特性的补丁,你的测试需要能保证新特性是能正常工作的。当新特性不存在是,它们也是要失败的;一旦被应用又是能通过的。

    一个好方法是:首先在修改代码前,编写你的新测试。这种开发风格被称为,而且既能被应用在整个项目又能应用在单个补丁。在编写完你的测试后,运行一下它们以确保它们确实是失败的(由于你还没有修复 bug 或者新增特性)。如果你的新测试没有失败,你就需要修复一下这些测试好让它们失败。毕竟,无论 bug 是否存在,回退测试都会通过,那在防止 bug 重新出现这事上是非常没有帮助的。

    现在让我们动手做下实例。

    为任务 #24788 编写一些测试

    任务 #24788 推荐加点小功能:给 Form 类指定类级别(class level)属性加 前缀(prefix) 的功能:

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

    切换到 Django 的 tests/forms_tests/tests/ 文件夹里,打开 test_forms.py 文件。在第 1674 行 test_forms_with_null_boolean 函数之前添加以下代码:

    1. # tests/forms_tests/tests/test_forms.py
    2. def test_class_prefix(self):
    3. # 前缀也可以在类级别指定
    4. class Person(Form):
    5. first_name = CharField()
    6. p = Person()
    7. self.assertEqual(p.prefix, 'foo')
    8. p = Person(prefix='bar')
    9. self.assertEqual(p.prefix, 'bar')

    这个新的测试:检查 [设置一个类级别前缀] 是否如预期一样地工作;并且在创建实例时传递一个 前缀(prefix) 参数,看是否也工作。

    但这个测试的东西看起来特别困难……

    如果你之前从来没有接触过测试,第一眼看上去它们确实有点难。幸运的是,测试在电脑编程中是个 非常 大的主题,有很多出自这里的信息:

    • 编写和运行测试文档 —— 给 Django 编写测试。
    • —— 《深入 Python》(一本给 Python 初学开发者的免费的在线书籍)。
    • 在阅读完这些后,如果你求知若渴,还可以看看 Python 的官方文档 unittest

    运行你写的新测试

    请记住,我们实际上没有对 BaseForm 做任何修改,所以我们的测试应该会失败。为了确保失败真的出现了,让我们在 forms_tests 文件夹运行一下所有的测试。从命令行里, cd 切换进 Django 的 tests/ 目录里,然后运行:

    1. $ ./runtests.py forms_tests

    如果测试运行正常,你应该看到和我们添加的测试方法相对应的一个失败。如果所有的测试都通过了,那么你要确认一下你已经将新测试添加到了上面合适的文件夹和类里。

    下一步我们将给任务(ticket) 添加函数描述。

    切换到 django/django/forms/ 文件夹里,然后打开 forms.py。在第 72 行找到 BaseForm 类,然后在 field_order 属性之后添加 前缀(prefix) 类属性:

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

    一旦你修改完了 Django,我们就需要确保之前写的测试都能通过,这样我们才能看到我们上面写的代码正确工作。为了在 forms_tests 文件夹运行测试, cd 切换到 Django 的 tests/ 目录里,然后运行:

    1. $ ./runtests.py forms_tests

    噢,我们写的那些测试好了!您应该仍然会看到一个例外失败:

    我们忘记在 init() 方法里添加条件语句了。在 django/forms/forms.py 现在的第 87 行修改 self.prefix = prefix,添加一个条件语句:

    1. if prefix is not None:
    2. self.prefix = prefix

    重新运行测试,所有都应该是通过的。如果不是,请确认你像上面那样正确修改了 BaseForm 类,然后正确复制了新的测试。

    第二次运行 Django 的测试套件

    一旦你验证了你的补丁,你的测试都是正常工作的,运行整个测试套件是个好主意,去验证你的修改没有将 bug 引入到 Django 的其他地方。整个测试套件成功通过并不保证你的代码是没有 bug 的,尽管它确实有助于识别很多可能被忽视的错误和回退。

    为了运行整个 Django 测试套件, cd 切换到 Django 的 tests/ 目录里,然后运行:

    1. $ ./runtests.py

    只要你没看到任何失败,你就可以继续了。

    写文档

    这是个新功能,所以应该被记录到文档里。在 django/docs/ref/forms/api.txt 的第 1068 行(文件结尾)添加:

    1. The prefix can also be specified on the form class::
    2. >>> class PersonForm(forms.Form):
    3. ... ...
    4. ... prefix = 'person'
    5. .. versionadded:: 1.9
    6. The ability to specify ``prefix`` on the form class was added.

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

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

    有关编写文档的更多信息,包括有关 versionadded 的说明,可以查看文档编写。文章还包括了一篇关于怎样在本地建立文档副本的说明,好让你可以预览将被生成的 HTML。

    预览你的修改

    现在是时候看看我们的补丁做的所有修改了。为了显示你当前的 Django 副本(有你的修改)和你最初的版本:

    1. $ git diff

    使用箭头键上下移动。

    1. diff --git a/django/forms/forms.py b/django/forms/forms.py
    2. index 509709f..d1370de 100644
    3. --- a/django/forms/forms.py
    4. +++ b/django/forms/forms.py
    5. @@ -75,6 +75,7 @@ class BaseForm(object):
    6. # information. Any improvements to the form API should be made to *this*
    7. # class, not to the Form class.
    8. field_order = None
    9. + prefix = None
    10. def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    11. initial=None, error_class=ErrorList, label_suffix=None,
    12. @@ -83,7 +84,8 @@ class BaseForm(object):
    13. self.data = data or {}
    14. self.files = files or {}
    15. self.auto_id = auto_id
    16. - self.prefix = prefix
    17. + if prefix is not None:
    18. + self.prefix = prefix
    19. self.initial = initial or {}
    20. self.error_class = error_class
    21. # Translators: This is the default suffix added to form field labels
    22. diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
    23. --- a/docs/ref/forms/api.txt
    24. +++ b/docs/ref/forms/api.txt
    25. @@ -1065,3 +1065,13 @@ You can put several Django forms inside one ``<form>`` tag. To give each
    26. >>> print(father.as_ul())
    27. <li><label for="id_father-first_name">First name:</label> <input type="text" name="father-first_name" id="id_father-first_name" /></li>
    28. <li><label for="id_father-last_name">Last name:</label> <input type="text" name="father-last_name" id="id_father-last_name" /></li>
    29. +
    30. +The prefix can also be specified on the form class::
    31. +
    32. + ... ...
    33. + ... prefix = 'person'
    34. +
    35. +.. versionadded:: 1.9
    36. +
    37. + The ability to specify ``prefix`` on the form class was added.
    38. diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
    39. index 5b58f79..f9bb9de 100644
    40. --- a/docs/releases/1.9.txt
    41. +++ b/docs/releases/1.9.txt
    42. @@ -161,6 +161,9 @@ Forms
    43. :attr:`~django.forms.Form.field_order` attribute, the ``field_order``
    44. constructor argument , or the :meth:`~django.forms.Form.order_fields` method.
    45. +* A form prefix can be specified inside a form class, not only when
    46. + instantiating a form. See :ref:`form-prefix` for details.
    47. +
    48. Generic Views
    49. ^^^^^^^^^^^^^
    50. diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
    51. index 690f205..e07fae2 100644
    52. --- a/tests/forms_tests/tests/test_forms.py
    53. +++ b/tests/forms_tests/tests/test_forms.py
    54. @@ -1671,6 +1671,18 @@ class FormsTestCase(SimpleTestCase):
    55. self.assertEqual(p.cleaned_data['last_name'], 'Lennon')
    56. self.assertEqual(p.cleaned_data['birthday'], datetime.date(1940, 10, 9))
    57. + def test_class_prefix(self):
    58. + # Prefix can be also specified at the class level.
    59. + class Person(Form):
    60. + first_name = CharField()
    61. + prefix = 'foo'
    62. +
    63. + p = Person()
    64. + self.assertEqual(p.prefix, 'foo')
    65. +
    66. + p = Person(prefix='bar')
    67. + self.assertEqual(p.prefix, 'bar')
    68. +
    69. def test_forms_with_null_boolean(self):
    70. # NullBooleanField is a bit of a special case because its presentation (widget)
    71. # is different than its data. This is handled transparently, though.

    当你预览完了这个补丁,按下 q 键回到命令行。如果补丁的内容看起来是对的,那就是时候 提交(commit) 这些修改了。

    提交补丁中的修改

    为了提交修改:

    为了输入提交的信息,会打开一个文本编辑器。根据,写下像这样的信息:

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

    推送提交和拉取请求(pull request)

    在提交了补丁之后,把它发送到在 GitHub 上你 fork 下来的仓库(如果不一样,就把 “ticket_24788”替换为分支的名字):

    通过 Django 的 GitHub 页面,你可以创建一个拉取请求。你会看到你的分支在 “你最近推送的分支(Your recently pushed branches)” 里。点击旁边的 “对比和拉取请求(Compare & pull request)”。

    但在本教程里,请别那么做,在下一页里显示预览补丁,你可以点击 “创建拉取请求(Create pull request)”

    恭喜你,你已经学会了如何给 Django 创建拉取请求了!更多进阶技术细节你可以看。

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

    在你给 Django 写补丁之前,这里有些关于贡献的信息,你应该看一下:

    • 你应该确保读了 Django 的文档认领任务和提交补丁。它涵盖了 Trac 礼仪,如何申请自己的任务,期望的补丁代码风格以及很多其他的重要细节。
    • 读完这些后,如果你仍然渴望得到更多关于贡献的信息,你可以随时浏览 的剩下部分。那里包含了非常多的信息,也应该是回答你任何问题的第一来源。

    一旦你已经看完了所有的那些信息,你就准备好入门了,然后寻找属于你的任务,去编写补丁吧。特别留意那些写着 “轻松(easy pickings)” 级别的任务。这些任务通常更简单,对于首次贡献者而言是非常棒的。一旦你熟悉了为 Django 做贡献,你就可以继续为更困难更复杂的任务编写补丁了。

    在任务有了补丁之后,它需要被第二次审查。在提交了拉取请求后,通过设置标志在任务上,比如 “有补丁了(has patch”)”,“不用测试了(doesn’t need tests)” 等,来更新任务元数据,好让其他人为了审查而找到它。做贡献不意味着总是从头开始写补丁。审查已经存在的补丁也是非常有帮助的一次贡献。查看 Triaging tickets 获取更多细节。