如何编写可安装的 Django 应用程序
目录
在Django框架中,项目是指特定网站的配置文件和代码的集合。Django 将业务逻辑分组到它所谓的应用程序中,它们是 Django 框架的模块。有很多关于如何构建项目和其中的应用程序的文档,但是当需要打包可安装的 Django 应用程序时,信息就更难找到了。
在本教程中,您将学习如何从Django 项目中取出应用程序并将其打包以使其可安装。打包应用程序后,您可以在PyPI上共享它,以便其他人可以通过pip install
在本教程中,您将学习:
- 编写独立应用程序和在项目内部编写应用程序有什么区别
- 如何创建用于发布 Django 应用程序的
setup.cfg
文件 - 如何在Django 项目之外引导 Django,以便您可以测试您的应用程序
- 如何使用 Python 和 Django 的多个版本进行测试
tox
- 如何使用Twine将可安装的 Django 应用程序发布到 PyPI
先决条件
本教程需要对Django、pip
、PyPI、pyenv
(或等效的虚拟环境工具)和tox
. 要了解有关这些主题的更多信息,请查看:
- Django Tutorials
- What Is Pip? A Guide for New Pythonistas
- How to Publish an Open-Source Python Package to PyPI
- Managing Multiple Python Versions With pyenv
- Getting Started With Testing in Python
在项目中启动示例 Django 应用程序
本教程包含一个工作包,可帮助指导您完成制作可安装 Django 应用程序的过程。
即使您最初打算将 Django 应用程序作为包提供,您也可能会从项目内部开始。为了演示从 Django 项目转移到可安装的 Django 应用程序的过程,我在 repo 中提供了两个分支。该项目的分支是一个Django项目的内部应用程序的起始状态。该主分支是完成安装的应用程序。
您还可以在PyPI realpython-django-receipts 包页面下载完成的应用程序。您可以通过运行安装该软件包pip install realpython-django-receipts
。
示例应用程序是收据上行项目的简短表示。在项目分支中,您将找到一个名为的目录sample_project
,其中包含一个正在运行的 Django 项目。该目录如下所示:
sample_project/
│
├── receipts/
│ ├── fixtures/
│ │ └── receipts.json
│ │
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
│
├── sample_project/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
│
├── db.sqlite3
├── manage.py
├── resetdb.sh
└── runserver.sh
在编写本教程时,Django 的最新版本是 3.0.4,所有测试都是使用 Python 3.7 完成的。本教程中概述的所有步骤都不应与早期版本的 Django 不兼容——我从 Django 1.8 开始使用这些技术。但是,如果您使用的是 Python 2,则需要进行一些更改。为了使示例简单,我假设整个代码库都使用 Python 3.7。
从头开始创建 Django 项目
示例项目和收据应用程序是使用 Djangoadmin
命令和一些小的编辑创建的。首先,在干净的虚拟环境中运行以下代码:
$ python -m pip install Django
$ django-admin startproject sample_project
$ cd sample_project
$ ./manage.py startapp receipts
这将创建sample_project
项目目录结构和receipts
应用程序子目录,其中包含您将用于创建可安装 Django 应用程序的模板文件。
接下来,该sample_project/settings.py
文件需要进行一些修改:
- 添加
'127.0.0.1'
到ALLOWED_HOSTS
设置中,以便您可以在本地进行测试。 - 添加
'receipts'
到INSTALLED_APPS
列表中。
您还需要receipts
在sample_project/urls.py
文件中注册应用程序的 URL 。为此,请添加path('receipts/', include('receipts.urls'))
到url_patterns
列表中。
探索收据示例应用程序
该应用程序由两个 ORM 模型类组成:Item
和Receipt
. 本Item
类包含了描述和成本数据库字段声明。成本包含在DecimalField
. 使用浮点数来表示货币是危险的——在处理货币时你应该总是使用定点数。
该Receipt
班是一个收集点Item
的对象。这是一个实现ForeignKey
在Item
该点Receipt
。Receipt
还包括total()
获取Item
包含在 中的对象的总成本Receipt
:
# receipts/models.py
from decimal import Decimal
from django.db import models
class Receipt(models.Model):
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Receipt(id={self.id})"
def total(self) -> Decimal:
return sum(item.cost for item in self.item_set.all())
class Item(models.Model):
created = models.DateTimeField(auto_now_add=True)
description = models.TextField()
cost = models.DecimalField(max_digits=7, decimal_places=2)
receipt = models.ForeignKey(Receipt, on_delete=models.CASCADE)
def __str__(self):
return f"Item(id={self.id}, description={self.description}, " \
f"cost={self.cost})"
模型对象为您提供数据库内容。一个简短的 Django 视图返回一个JSON 字典,其中Receipt
包含Item
数据库中的所有对象及其对象:
# receipts/views.py
from django.http import JsonResponse
from receipts.models import Receipt
def receipt_json(request):
results = {
"receipts":[],
}
for receipt in Receipt.objects.all():
line = [str(receipt), []]
for item in receipt.item_set.all():
line[1].append(str(item))
results["receipts"].append(line)
return JsonResponse(results)
的receipt_json()
视图遍历所有的Receipt
目的,创建一对所述的Receipt
对象和所述的列表Item
的对象包含在。所有这些都放在字典中并通过 Django 的JsonResponse()
.
为了使模型在Django 管理界面中可用,您可以使用一个admin.py
文件来注册模型:
# receipts/admin.py
from django.contrib import admin
from receipts.models import Receipt, Item
@admin.register(Receipt)
class ReceiptAdmin(admin.ModelAdmin):
pass
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
pass
此代码ModelAdmin
为每个Receipt
和Item
类创建一个 Django ,并将它们注册到 Django 管理员。
最后,urls.py
文件在应用程序中针对 URL 注册单个视图:
# receipts/urls.py
from django.urls import path
from receipts import views
urlpatterns = [
path("receipt_json/", views.receipt_json),
]
您现在可以包含receipts/urls.py
在您的项目url.py
文件中,以便在您的网站上提供收据视图。
一切就绪后,可以运行./manage.py makemigrations receipts
,使用Django admin添加数据,然后访问/receipts/receipt_json/
查看结果:
$ curl -sS http://127.0.0.1:8000/receipts/receipt_json/ | python3.8 -m json.tool
{
"receipts": [
[
"Receipt(id=1)",
[
"Item(id=1, description=wine, cost=15.25)",
"Item(id=2, description=pasta, cost=22.30)"
]
],
[
"Receipt(id=2)",
[
"Item(id=3, description=beer, cost=8.50)",
"Item(id=4, description=pizza, cost=12.80)"
]
]
]
}
在上面的块中,您使用curl
访问receipt_json
视图,从而产生包含Receipt
对象及其对应Item
对象的 JSON 响应。
在项目中测试 App
Djangounittest
用自己的测试功能扩充了 Python包,使您能够将设备预加载到数据库中并运行您的测试。收据应用程序定义了一个tests.py
文件和一个用于测试的装置。这个测试绝不是全面的,但它是一个足够好的概念证明:
# receipts/tests.py
from decimal import Decimal
from django.test import TestCase
from receipts.models import Receipt
class ReceiptTest(TestCase):
fixtures = ["receipts.json", ]
def test_receipt(self):
receipt = Receipt.objects.get(id=1)
total = receipt.total()
expected = Decimal("37.55")
self.assertEqual(expected, total)
夹具创建两个Receipt
对象和四个对应的Item
对象。单击下面的可折叠部分以仔细查看夹具的代码。
receives.json 测试装置显示隐藏
您可以使用 Djangomanage.py
命令测试收据应用程序:
$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.013s
OK
Destroying test database for alias 'default'...
Runningmanage.py test
运行 中定义的单个测试receipts/tests.py
并显示结果。
制作可安装的 Django 应用
您的目标是在没有项目的情况下共享收据应用程序,并使其可被其他人重复使用。您可以压缩receipts/
目录并将其分发出去,但这有些限制。相反,您希望将应用程序分成一个包,以便它可以安装。
创建可安装的 Django 应用程序的最大挑战是 Django 需要一个项目。没有项目的应用程序只是一个包含代码的目录。没有项目,Django 不知道如何处理您的代码,包括运行测试。
将您的 Django 应用程序移出项目
保留一个示例项目是个好主意,这样您就可以运行 Django 开发服务器并使用您的应用程序的实时版本。您不会将此示例项目包含在应用程序包中,但它仍然可以存在于您的存储库中。按照这个想法,您可以通过将可安装的 Django 应用程序上移到一个目录来开始打包它:
$ mv receipts ..
目录结构现在看起来像这样:
django-receipts/
│
├── receipts/
│ ├── fixtures/
│ │ └── receipts.json
│ │
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │
│ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ ├── views.py
│ ├── admin.py
│ └── apps.py
│
├── sample_project/
│ ├── sample_project/
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ │
│ ├── db.sqlite3
│ ├── manage.py
│ ├── resetdb.sh
│ └── runserver.sh
│
├── LICENSE
└── README.rst
要打包您的应用程序,您需要将其从项目中拉出。移动它是第一步。我通常会保留原始项目以进行测试,但我不会将其包含在生成的包中。
在项目外引导 Django
现在您的应用程序在 Django 项目之外,您需要告诉 Django 如何找到它。如果您想测试您的应用程序,请运行一个可以找到您的应用程序或运行您的迁移的 Django shell 。您需要配置 Django 并使其可用。
Django's settings.configure()
anddjango.setup()
是在项目之外与您的应用程序交互的关键。有关这些调用的更多信息,请参阅 Django 文档。
您可能在多个地方需要 Django 的这种配置,因此在函数中定义它是有意义的。创建一个名为的文件,boot_django.py
其中包含以下代码:
1# boot_django.py
2#
3# This file sets up and configures Django. It's used by scripts that need to
4# execute as if running in a Django server.
5import os
6import django
7from django.conf import settings
8
9BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "receipts"))
10
11def boot_django():
12 settings.configure(
13 BASE_DIR=BASE_DIR,
14 DEBUG=True,
15 DATABASES={
16 "default":{
17 "ENGINE":"django.db.backends.sqlite3",
18 "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
19 }
20 },
21 INSTALLED_APPS=(
22 "receipts",
23 ),
24 TIME_ZONE="UTC",
25 USE_TZ=True,
26 )
27 django.setup()
第 12 和 27 行设置了 Django 环境。该settings.configure()
调用采用与settings.py
文件中定义的变量等效的参数列表。settings.py
使您的应用程序运行所需的任何内容都会传递到settings.configure()
.
上面的代码是一个相当精简的配置。收据应用程序不会对会话或模板执行任何操作,因此INSTALLED_APPS
只需要"receipts"
,并且您可以跳过任何中间件定义。该USE_TZ=True
值是必需的,因为Receipt
模型包含created
时间戳。否则,您将在加载测试夹具时遇到问题。
使用可安装的 Django 应用程序运行管理命令
现在您有了boot_django.py
,您可以使用一个非常短的脚本运行任何 Django 管理命令:
#!/usr/bin/env python
# makemigrations.py
from django.core.management import call_command
from boot_django import boot_django
boot_django()
call_command("makemigrations", "receipts")
Django 允许您通过call_command()
. 您现在可以通过导入和调用boot_django()
后跟call_command()
.
你的应用程序现在在项目之外,允许你对它做各种各样的 Django-y 事情。我经常定义四个实用程序脚本:
load_tests.py
测试您的应用makemigrations.py
创建迁移文件migrate.py
执行表迁移djangoshell.py
生成一个知道你的应用程序的 Django shell
测试可安装的 Django 应用
该load_test.py
文件可以像makemigrations.py
脚本一样简单,但它只能一次运行所有测试。通过添加几行,您可以将命令行参数传递给测试运行器,从而允许您运行选择性测试:
1#!/usr/bin/env python
2# load_tests.py
3import sys
4from unittest import TestSuite
5from boot_django import boot_django
6
7boot_django()
8
9default_labels = ["receipts.tests", ]
10
11def get_suite(labels=default_labels):
12 from django.test.runner import DiscoverRunner
13 runner = DiscoverRunner(verbosity=1)
14 failures = runner.run_tests(labels)
15 if failures:
16 sys.exit(failures)
17
18 # In case this is called from setuptools, return a test suite
19 return TestSuite()
20
21if __name__ == "__main__":
22 labels = default_labels
23 if len(sys.argv[1:]) > 0:
24 labels = sys.argv[1:]
25
26 get_suite(labels)
DjangoDiscoverRunner
是一个测试发现类,与 Python 的unittest
. 它负责设置测试环境、构建测试套件、设置数据库、运行测试,然后将其全部拆除。从第 11 行开始,get_suite()
获取测试标签列表并直接调用DiscoverRunner
它们。
该脚本类似于 Django 管理命令的test
作用。该__main__
块将任何命令行参数传递给get_suite()
,如果没有,则它将传递到应用程序的测试套件中receipts.tests
。您现在可以load_tests.py
使用测试标签参数调用并运行单个测试。
第 19 行是一个特殊情况,可在使用tox
. 您将tox
在后面的部分中了解更多信息。您还可以DiscoverRunner
在下面的可折叠部分中查看潜在的替代品。
DiscoverRunner 替代方案显示隐藏
定义您的可安装包 setup.cfg
要将可安装的 Django 应用程序放在 PyPI 上,您需要先将其放入一个包中。PyPI中期望一个egg
,wheel
或源分布。这些是使用setuptools
. 为此,您需要在与目录相同的目录级别创建一个setup.cfg
文件和一个setup.py
文件receipts
。
但是,在深入研究之前,您需要确保您有一些文档。您可以在 中包含项目描述setup.cfg
,它会自动显示在 PyPI 项目页面上。确保写一个README.rst
或类似的关于你的包的信息。
PyPI默认支持reStructuredText格式,但它也可以处理带有额外参数的Markdown:
1# setup.cfg
2[metadata]
3name = realpython-django-receipts
4version = 1.0.3
5description = Sample installable django app
6long_description = file:README.rst
7url = https://github.com/realpython/django-receipts
8license = MIT
9classifiers =
10 Development Status :: 4 - Beta
11 Environment :: Web Environment
12 Intended Audience :: Developers
13 License :: OSI Approved :: MIT License
14 Operating System :: OS Independent
15 Programming Language :: Python :: 3 :: Only
16 Programming Language :: Python :: 3.7
17 Programming Language :: Python :: Implementation :: CPython
18 Topic :: Software Development :: Libraries :: Application Frameworks
19 Topic :: Software Development :: Libraries :: Python Modules
20
21[options]
22include_package_data = true
23python_requires = >=3.6
24setup_requires =
25 setuptools >= 38.3.0
26install_requires =
27 Django>=2.2
该setup.cfg
文件描述了您将构建的包。第 6 行使用file:
指令读取您的README.rst
文件。这使您不必在两个地方编写长描述。
第26 行的install_requires
条目告诉任何安装程序,例如,您的应用程序具有的依赖项。您将始终希望将可安装的 Django 应用程序与其支持的最低 Django 版本联系起来。pip install
如果您的代码具有任何仅运行测试所需的依赖tests_require =
项,则您可以添加一个条目。例如,在mock
成为标准 Python 库的一部分之前,tests_require = mock>=2.0.0
在setup.cfg
.
pyproject.toml
在你的包中包含一个文件被认为是最佳实践。布雷特·坎农 (Brett Cannon)关于该主题的出色文章可以让您了解详细信息。pyproject.toml
示例代码中还包含一个文件。
您几乎已准备好为可安装的 Django 应用程序构建包。测试它的最简单方法是使用您的示例项目——另一个保留示例项目的好理由。该pip install
命令支持本地定义的包。这可用于确保您的应用程序仍然适用于项目。但是,一个警告是setup.cfg
在这种情况下它不会单独工作。您还必须创建一个 shim 版本setup.py
:
#!/usr/bin/env python
if __name__ == "__main__":
import setuptools
setuptools.setup()
此脚本将自动使用您的setup.cfg
文件。您现在可以安装包的本地可编辑版本以从sample_project
. 可以肯定的是,最好从一个全新的虚拟环境开始。requirements.txt
在sample_project
目录中添加以下文件:
# requirements.txt
-e ../../django-receipts
该-e
告诉pip
,这是一个本地可编辑的安装。您现在已准备好安装:
$ pip install -r requirements.txt
Obtaining django-receipts (from -r requirements.txt (line 1))
Collecting Django>=3.0
Using cached Django-3.0.4-py3-none-any.whl (7.5 MB)
Collecting asgiref~=3.2
Using cached asgiref-3.2.7-py2.py3-none-any.whl (19 kB)
Collecting pytz
Using cached pytz-2019.3-py2.py3-none-any.whl (509 kB)
Collecting sqlparse>=0.2.2
Using cached sqlparse-0.3.1-py2.py3-none-any.whl (40 kB)
Installing collected packages: asgiref, pytz, sqlparse, Django, realpython-django-receipts
Running setup.py develop for realpython-django-receipts
Successfully installed Django-3.0.4 asgiref-3.2.7 pytz-2019.3 realpython-django-receipts sqlparse-0.3.1
该install_requires
列表setup.cfg
告诉pip install
它需要Django的。Django 需要asgiref
, pytz
, 和sqlparse
. 所有依赖项都已处理完毕,您现在应该可以运行sample_project
Django 开发服务器了。恭喜 - 您的应用程序现在已从示例项目中打包和引用!
测试多个版本 tox
Django 和 Python 都在不断进步。如果您要与全世界共享您的可安装 Django 应用程序,那么您可能需要在多种环境中进行测试。该tox
工具需要一点帮助才能测试您的 Django 应用程序。继续并在内部进行以下更改setup.cfg
:
1# setup.cfg
2[metadata]
3name = realpython-django-receipts
4version = 1.0.3
5description = Sample installable django app
6long_description = file:README.rst
7url = https://github.com/realpython/django-receipts
8license = MIT
9classifiers =
10 Development Status :: 4 - Beta
11 Environment :: Web Environment
12 Intended Audience :: Developers
13 License :: OSI Approved :: MIT License
14 Operating System :: OS Independent
15 Programming Language :: Python :: 3 :: Only
16 Programming Language :: Python :: 3.7
17 Programming Language :: Python :: Implementation :: CPython
18 Topic :: Software Development :: Libraries :: Application Frameworks
19 Topic :: Software Development :: Libraries :: Python Modules
20
21[options]
22include_package_data = true
23python_requires = >=3.6
24setup_requires =
25 setuptools >= 38.3.0
26install_requires =
27 Django>=2.2
28test_suite = load_tests.get_suite
第 28 行告诉包管理器使用load_tests.py
脚本来获取它的测试套件。该tox
实用程序使用它来运行其测试。记得get_suite()
在load_tests.py
:
1# Defined inside load_tests.py
2def get_suite(labels=default_labels):
3 from django.test.runner import DiscoverRunner
4 runner = DiscoverRunner(verbosity=1)
5 failures = runner.run_tests(labels)
6 if failures:
7 sys.exit(failures)
8
9 # If this is called from setuptools, then return a test suite
10 return TestSuite()
诚然,这里发生的事情有点奇怪。通常,test_suite
字段 insetup.cfg
指向返回一组测试的方法。当tox
调用时setup.py
,它读取test_suite
参数并运行load_tests.get_suite()
。
如果这个调用没有返回一个TestSuite
对象,那么tox
就会抱怨。奇怪的是,您实际上并不想tox
获得一套测试,因为tox
您不知道 Django 测试环境。相反,在第 10 行get_suite()
创建 aDiscoverRunner
并返回一个空TestSuite
对象。
您不能简单地DiscoverRunner
返回一组测试,因为您必须调用DiscoverRunner.run_tests()
Django 测试环境的设置和拆卸才能正确执行。仅仅通过正确的测试是tox
行不通的,因为不会创建数据库。get_suite()
运行所有测试,但作为函数调用的副作用而不是作为返回测试套件tox
执行的正常情况。
该tox
工具允许您测试多个组合。甲tox.ini
文件确定测试环境的哪个组合。下面是一个例子:
[tox]
envlist = py{36,37}-django220, py{36,37}-django300
[testenv]
deps =
django220: Django>=2.2,<3
django300: Django>=3
commands=
python setup.py test
该文件指出,应针对 Python 3.6 和 3.7 以及 Django 2.2 和 3.0 运行测试。总共有四个测试环境。该commands=
部分是您告诉tox
通过 调用测试的地方setup.py
。这就是您test_suite = load_tests.get_suite
在setup.cfg
.
注意:的test
子命令setup.py
已被弃用。目前,Python 中的打包正在迅速变化。虽然python setup.py test
通常不推荐调用,但在这种特定情况下它是有效的。
发布到 PyPI
最后,是时候在 PyPI 上分享您的可安装 Django 应用程序了。有多种工具可用于上传包,但在本教程中您将重点介绍Twine。以下代码构建包并调用 Twine:
$ python -m pip install -U wheel twine setuptools
$ python setup.py sdist
$ python setup.py bdist_wheel
$ twine upload dist/*
前两个命令构建包的源代码和二进制分发版。调用twine
上传到 PyPI。如果您.pypirc
的主目录中有一个文件,那么您可以预设您的用户名,这样系统只会提示您输入密码:
[disutils]
index-servers =
pypi
[pypi]
username: <YOUR_USERNAME>
我经常使用一个小的shell脚本来grep
从代码中获取版本号。然后我调用git tag
用版本号标记repo,删除旧的build/
和dist/
目录,并调用上述三个命令。
有关使用 Twine 的更多详细信息,请参阅如何将开源 Python 包发布到 PyPI。Twine 的两个流行替代品是Poetry和Flit。Python 中的包管理正在迅速变化。PEP 517和PEP 518正在重新定义如何描述 Python 包和依赖项。
结论
Django 应用程序依赖于 Django 项目结构,因此单独打包它们需要额外的步骤。您已经了解了如何通过从项目中提取、打包并在 PyPI 上共享来制作可安装的 Django 应用程序。请务必从以下链接下载示例代码:
在本教程中,您学习了如何:
- 在项目之外使用Django 框架
- 在独立于项目的应用程序上调用 Django管理命令
- 编写一个调用Django 测试的脚本,可选择使用单个测试标签
- 构建一个
setup.py
文件来定义你的包 - 修改
setup.py
脚本以适应tox
- 使用Twine上传可安装的 Django 应用
您已准备好与全世界分享您的下一个应用程序。快乐编码!
进一步阅读
Django、打包、测试都是非常深奥的话题。那里有很多信息。要深入挖掘,请查看以下资源:
- Django 文档
- Django 入门:构建投资组合应用程序
- Django 教程
- 使用 pyenv 管理多个 Python 版本
- 什么是 pip?Python 新手指南
- 如何将开源 Python 包发布到 PyPi
- Python 测试入门
- Poetry
- Flit
PyPI有大量可安装的 Django 应用程序,值得一试。以下是一些最受欢迎的:
- 点赞
- 收藏
- 关注作者
评论(0)