参考https://realpython.com/pipenv-guide/#package-distribution
Pipenv: A Guide to the New Python Packaging Tool
Pipenv是Python的一个打包工具,它使用pip、virtualenv和旧的requirements.txt解决了与典型工作流相关的一些常见问题。
除了解决一些常见问题之外,它还将开发过程合并并简化为单个命令行工具。
本指南将详细讨论Pipenv解决了哪些问题,以及如何管理你与Pipenv的Python依赖关系。此外,还将介绍Pipenv如何适应以前的包分发方法。
Problems that Pipenv Solves
为了理解Pipenv的好处,了解当前Python中的打包和依赖项管理方法是很重要的。
让我们从处理第三方包的典型情况开始。然后,我们将构建部署完整Python应用程序的方法。
Dependency Management with requirements.txt
假设您正在处理一个使用类似于flask的第三方包的Python项目。您需要指定该需求,以便其他开发人员和自动化系统可以运行您的应用程序。
所以你决定在requirements.txt文件中包含flask依赖,requirements.txt如下:
flask
很好,一切都在本地运行良好,在你的应用程序上运行一段时间后,你决定将它转移到生产环境中。这就是事情变得有点可怕的地方……
上面的requirements.txt文件没有指定使用哪个版本的flask。在这种情况下,pip install -r requirementss .txt将默认安装最新版本。这是可以的,除非其在最新版本中有接口或行为的变化,这时我们的应用程序可能会崩溃。
对于这个例子,我们假设发布了一个新的flask版本。但是,它向后兼容您在开发期间使用的版本。
现在,假设您将应用程序部署到生产环境中,并执行pip install -r requirements.txt。Pip获得了最新的、不向后兼容的flask版本,就像这样,您的应用程序在生产环境中就崩溃了。
“但是,嘿,它在我的机器上工作了!”“-我自己也经历过,感觉不太好。
此时,您知道您在开发期间使用的flask的版本工作得很好。因此,要修复这些问题,您可以尝试在requirements.txt中更具体一点。向flask依赖项添加版本说明符。这也叫做固定依赖:
flask==0.12.1
将flask依赖关系固定到特定的版本,可以确保pip install -r requirements.txt设置您在开发期间使用的flask的准确版本。但这是真的吗?
请记住,flask本身也有依赖项(pip会自动安装)。但是,flask本身并没有为它的依赖项指定精确的版本。例如,它允许任何版本的Werkzeug>=0.14。
同样,为了这个例子,让我们假设一个新版本的Werkzeug已经发布了,但是它给您的应用程序引入了一个无法阻止的bug。
当您在生产中执行pip install -r requirements.txt时,您将得到flask==0.12.1,因为您已经确定了这个要求。然而,不幸的是,您将得到最新的、有bug的Werkzeug版本。同样,产品在生产过程中损坏。
这里真正的问题是构建是不确定的。我的意思是,给定相同的输入(requirements.txt文件),pip并不总是生成相同的环境。目前,您无法在生产环境中轻松复制您的开发机器上的确切环境。
这个问题的典型解决方案是使用pip freeze。该命令允许您获得当前安装的所有第三方库的精确版本,包括自动安装的子依赖项pip。因此,您可以冻结开发中的所有内容,以确保在生产中拥有相同的环境。
执行pip freeze会导致固定的依赖关系,您可以将这些得到的固定依赖关系添加到requirements.txt中:
click==6.7 Flask==0.12.1 itsdangerous==0.24 Jinja2==2.10 MarkupSafe==1.0 Werkzeug==0.14.1
使用这些固定的依赖项,您可以确保在生产环境中安装的包与在开发环境中安装的包完全匹配,这样您的产品就不会意外中断。不幸的是,这种“解决方案”会导致一系列新的问题。
现在您已经指定了每个第三方包的确切版本,您需要负责使这些版本保持最新,即使它们是flask的子依赖项。如果在Werkzeug==0.14.1中发现了一个安全漏洞,而维护人员立即在Werkzeug==0.14.2中进行了修补,这时该怎么办?这时您确实需要更新到Werkzeug==0.14.2,以避免由早期未修补的Werkzeug版本引起的任何安全问题。
首先,您需要意识到您的版本有问题。然后,您需要在有人利用安全漏洞之前在生产环境中获得新版本。因此,您必须手动更改您的requirements.txt以指定新版本Werkzeug==0.14.2。正如您在这种情况下所看到的,保持必要更新的责任落在您身上。
事实是,只要Werkzeug不会破坏您的代码,您实际上并不关心安装的是什么版本的Werkzeug。事实上,您可能想要最新的版本来确保您得到了bug修复、安全补丁、新特性、更多优化等等。
真正的问题是:“在不负责更新子依赖项版本的情况下,如何为Python项目提供确定性的构建?”——即使用Pipenv
Development of Projects with Different Dependencies
让我们转换一下话题,讨论一下在您处理多个项目时出现的另一个常见问题。假设项目A需要django= 1.9,而项目B需要django= 1.10。
默认情况下,Python尝试将所有第三方包存储在一个系统范围内的位置。这意味着每次您想在项目A和项目B之间切换时,都必须确保安装了正确的django版本。这使得在项目之间的切换非常痛苦,因为您必须卸载和重新安装包来满足每个项目的需求。
标准的解决方案是使用具有自己的Python可执行文件和第三方包存储的虚拟环境。这样,项目A和项目B就被充分地分开了。现在您可以轻松地在项目之间切换,因为它们不共享相同的包存储位置。PackageA可以在自己的环境中拥有它需要的django的任何版本,而PackageB可以完全独立地拥有它需要的东西。一种非常常见的工具是virtualenv(或Python 3中的venv)。
Pipenv内置了虚拟环境管理,因此您可以使用一个单独的工具进行包管理。
Dependency Resolution
依赖性解析(dependency resolution)是什么意思?假设你有这样一个requirements.txt文件:
package_a
package_b
假设package_a有一个子依赖项package_c,而package_a需要此包的特定版本:package_c>=1.0。相应地,package_b具有相同的子依赖项,但需要package_c<=2.0。
理想情况下,当您尝试安装package_a和package_b时,安装工具将查看package_c的需求(>=1.0和<=2.0),并选择满足这些需求的版本。您希望该工具能够解析依赖项,以便您的程序最终能够工作。这就是我所说的“依赖性解析”。
不幸的是,pip本身目前还没有真正的依赖性解析方案,但是有一个有待解决的问题可以支持它。
pip处理上述场景的方式如下:
- 它安装package_a并查找满足第一个需求的package_c版本(package_c>=1.0)。
- 然后pip安装最新版本的package_c来满足这个需求。假设package_c的最新版本是3.1。
这就是(潜在的)麻烦开始的地方。
如果pip选择的package_c版本不符合将来的要求(例如package_b需要package_c<=2.0),安装将失败。
这个问题的“解决方案”是在requirements.txt文件中指定子依赖项(package_c)所需的范围。这样,pip就可以解决这个冲突,并安装一个满足这些要求的包:
package_c>=1.0,<=2.0 package_a package_b
就像之前一样,您现在直接关注子依赖项(package_c)。这里的问题是,如果package_a在您不知情的情况下更改了它们的需求,那么您指定的需求(package_c>=1.0,<=2.0)可能不再被接受,安装可能再次失败。真正的问题是,再一次,您要负责跟上子依赖项的需求。
理想情况下,您的安装工具应该足够智能,可以安装满足所有需求的包,而不必显式地指定子依赖项版本。
Pipenv Introduction
现在我们已经解决了这些问题,让我们看看Pipenv如何解决它们。
首先,让我们安装它:
pip install pipenv
一旦你这样做了,你可以直接忘记pip,因为pipenv实际上是它的一个替代品。它还引入了两个新文件pipfile(用于替换requirements.txt)和pipfile.lock(支持确定性构建)。
pipenv在底层使用了pip和virtualenv,但是通过一个命令行接口简化了它们的使用。
Example Usage
让我们从创建出色的Python应用程序开始。首先,在虚拟环境中生成一个shell来隔离这个app的开发:
pipenv shell
这将创建一个虚拟环境(如果还不存在的话,即等价于pipenv install)。Pipenv在默认位置创建所有虚拟环境。如果您想更改Pipenv的默认行为,有一些用于配置的环境变量(environmental variables for configuration)。
您可以分别使用--two和--three强制创建Python 2或3环境。否则,Pipenv将使用virtualenv找到的任何默认值。
⚠️如果你需要python更具体的版本,则使用--python 带着你需要的版本号,如--python 3.6
现在你可以安装你需要的第三方软件包,如flask。哦,但是你知道你需要的是0.12.1版本,而不是最新的版本,所以,写得具体点:
pipenv install flask==0.12.1
你将会在你的终端得到类似下面的内容:
Adding flask==0.12.1 to Pipfile's [packages]... Pipfile.lock not found, creating...
您将注意到创建了两个文件,一个Pipfile和一个Pipfile.lock。我们待会再仔细看看。让我们安装另一个第三方软件包numpy来处理一些数据。
因为你不需要一个特定的版本,所以可以不指定:
pipenv install numpy
如果你想直接从版本控制系统(VCS)安装一些东西,这也是能实现的! 您指定的位置类似于对pip的操作。
例如,要安装来自版本控制的请求库,请执行以下操作:
pipenv install -e git+https://github.com/requests/requests.git#egg=requests
请注意上面的-e参数,以使安装可编辑。目前,Pipenv需要执行子依赖项解析。
假设对于这个很棒的应用程序您也有一些单元测试,您想使用pytest来运行它们。
你不需要在生产中pytest,所以你可以使用--dev参数指定这个依赖关系只用于开发环境,不用于生产环境:
pipenv install pytest --dev
提供--dev参数将把依赖项放在Pipfile中一个特殊的[dev-packages]位置。只有在使用pipenv install指定--dev参数时,才会安装这些开发包。
不同的部分将仅用于开发的依赖项与基本代码实际工作所需的依赖项分开。通常,这可以通过附加的需求文件来完成,比如dev-requirements.txt或者test-requirements.txt。现在,所有内容都整合到一个单独的Pipfile中,放在不同的部分中。
好的,假设您已经在本地开发环境中完成了所有工作,并准备将其投入生产。要做到这一点,你需要锁定你的环境,这样你就可以确保你在生产环境中有一个与开发环境相同的环境:
pipenv lock
这将创建/更新您的Pipfile.lock文件,您永远不需要(也永远不打算)手动编辑。您应该始终使用生成的文件。
现在,一旦在你的生产环境获得了所需的代码和Pipfile.lock,你将在生产环境中安装上最后记录的成功的环境:
pipenv install --ignore-pipfile
这告诉Pipenv忽略用于安装的Pipfile文件并使用Pipfile.lock中的内容。鉴于Pipfile.lock,Pipenv会创建你运行Pipenv lock时得到的完全相同的环境,包括子依赖项等等。
锁文件通过捕获环境中包的所有版本的快照(类似于pip freeze的结果)来支持确定性构建。
现在让我们假设另一个开发人员想要对您的代码做一些添加。在这种情况下,他们会得到代码,包括Pipfile,并使用这个命令:
pipenv install --dev
这将安装开发所需的所有依赖项,包括常规依赖项和安装期间使用--dev参数指定的依赖项。
⚠️当Pipfile中没有指定确切的版本时,install命令为依赖项(和子依赖项)提供了更新其版本的机会。
这是一个重要的注意事项,因为它解决了我们之前讨论的一些问题。为了进行演示,假设您的一个依赖项的新版本可用。因为不需要这个依赖项的特定版本,所以不需要在Pipfile文件中指定确切的版本。当您安装pipenv时,依赖项的新版本将安装到您的开发环境中。
现在,您可以对代码进行更改,并运行一些测试来验证一切是否仍按预期工作。(你有单元测试,对吧?)现在,与以前一样,您使用pipenv lock来锁定您的环境,这样一个新的带着新版本的依赖项的Pipfile.lock文件就会生成。与前面一样,您可以使用锁文件在生产环境中复制这个新环境。
正如您从这个场景中看到的,您不再需要强制使用不需要的确切版本来确保开发和生产环境是相同的。您也不需要随时更新您“不关心”的子依赖项。这个使用Pipenv并结合了您出色的测试的工作流程, 能够修复手动执行所有依赖项管理的问题。
Pipenv’s Dependency Resolution Approach
Pipenv将尝试安装满足您的核心依赖项的所有需求的子依赖项。但是,如果存在相互冲突的依赖关系(package_a需要package_c>=1.0,而package_b需要package_c<1.0), Pipenv将无法创建锁文件,并将输出如下错误:
Warning: Your dependencies could not be resolved. You likely have a mismatch in your sub-dependencies. You can use $ pipenv install --skip-lock to bypass this mechanism, then run $ pipenv graph to inspect the situation. Could not find a version that matches package_c>=1.0,package_c<1.0
正如警告所说,您还可以显示依赖关系图,以了解您的顶级依赖关系及其子依赖关系:
pipenv graph
此命令将打印出一个类似于树的结构,显示您的依赖项。这里有一个例子:
Flask==0.12.1 - click [required: >=2.0, installed: 6.7] - itsdangerous [required: >=0.21, installed: 0.24] - Jinja2 [required: >=2.4, installed: 2.10] - MarkupSafe [required: >=0.23, installed: 1.0] - Werkzeug [required: >=0.7, installed: 0.14.1] numpy==1.14.1 pytest==3.4.1 - attrs [required: >=17.2.0, installed: 17.4.0] - funcsigs [required: Any, installed: 1.0.2] - pluggy [required: <0.7,>=0.5, installed: 0.6.0] - py [required: >=1.5.0, installed: 1.5.2] - setuptools [required: Any, installed: 38.5.1] - six [required: >=1.10.0, installed: 1.11.0] requests==2.18.4 - certifi [required: >=2017.4.17, installed: 2018.1.18] - chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4] - idna [required: >=2.5,<2.7, installed: 2.6] - urllib3 [required: <1.23,>=1.21.1, installed: 1.22]
从pipenv graph的输出中,您可以看到我们以前安装的顶级依赖项(Flask、numpy、pytest和requests),在下面您可以看到它们所依赖的包。
此外,你可以反转树来显示需要它的子依赖关系:
pipenv graph --reverse
当您试图找出相互冲突的子依赖项时,这种反向树可能更有用。
The Pipfile
Pipfile打算替换requirements.txt。Pipenv是当前使用Pipfile的参考实现。看起来,pip本身很可能能够处理这些文件。另外,值得注意的是,Pipenv甚至是Python本身推荐的官方包管理工具。
Pipfile的语法是TOML,文件被分成几个部分。[dev-packages]记录只用于开发的包,[packages]记录最少需要的包,[requires]声明其他需求,比如特定版本的Python。参见下面的示例文件:
[[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [dev-packages] pytest = "*" [packages] flask = "==0.12.1" numpy = "*" requests = {git = "https://github.com/requests/requests.git", editable = true} [requires] python_version = "3.6"
理想情况下,Pipfile中不应该有任何子依赖项。我的意思是,您应该只包含您实际导入和使用的包。不需要将chardet保存在Pipfile中,因为它是请requests的子依赖项。(因为Pipenv会自动安装) Pipfile应该传递包需要的顶级依赖项。
The Pipfile.lock
此文件通过指定重现环境的确切需求来支持确定性构建。它包含包和散列的精确版本,以支持更安全的验证,pip本身现在也支持这种验证。示例文件可能如下所示。注意,这个文件的语法是JSON,我已经排除了部分文件…:
{ "_meta": { ... }, "default": { "flask": { "hashes": [ "sha256:6c3130c8927109a08225993e4e503de4ac4f2678678ae211b33b519c622a7242", "sha256:9dce4b6bfbb5b062181d3f7da8f727ff70c1156cbb4024351eafd426deb5fb88" ], "version": "==0.12.1" }, "requests": { "editable": true, "git": "https://github.com/requests/requests.git", "ref": "4ea09e49f7d518d365e7c6f7ff6ed9ca70d6ec2e" }, "werkzeug": { "hashes": [ "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b", "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c" ], "version": "==0.14.1" } ... }, "develop": { "pytest": { "hashes": [ "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", "sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6" ], "version": "==3.4.1" }, ... } }
请注意为每个依赖项指定的确切版本。即使像werkzeug这样不在Pipfile中的子依赖项也会出现在这个Pipfile.lock中。散列用于确保检索的包与在开发中检索的包相同。
值得注意的是,永远不要手动更改此文件。它是用pipenv lock命令生成的。
Pipenv Extra Features
使用以下命令在默认编辑器中打开第三方包:
pipenv open flask
这将在默认编辑器中打开flask包,或者您可以指定一个带有EDITOR(编辑器)环境变量的程序。例如,我使用了Sublime Text,因此我设置了EDITOR=subl。这使得深入研究您正在使用的包的内部变得超级简单。
您可以在虚拟环境中运行命令而无需启动shell:
pipenv run <insert command here>
检查您的环境中的安全漏洞(和PEP 508要求):
pipenv check
现在,假设您不再需要一个包。你可以卸载它:
pipenv uninstall numpy
另外,假设您想要完全清除虚拟环境中安装的所有包:
pipenv uninstall --all
你可以使用--all-dev替换--all用以仅删除开发时使用的包
Pipenv支持在顶级目录中存在.env文件时自动加载环境变量。这样,当您使用pipenv shell打开虚拟环境时,它将从文件中加载您的环境变量。.env文件只包含键值对:
SOME_ENV_CONFIG=some_value
SOME_OTHER_ENV_CONFIG=some_other_value
最后,这里有一些快速的命令来找出东西的位置。如何找到你的虚拟环境:
pipenv --venv
如何找到你的项目所在地:
pipenv --where
显示Python解释器信息:
pipenv --py
Package Distribution
如果您打算将代码作为包分发,您可能会问这一切是如何工作的。
Yes, I need to distribute my code as a package
Pipenv如何处理setup.py文件?
这个问题有很多细微的差别。首先,在使用setuptools作为构建/分发系统时,需要一个setup.py文件。这是一段时间以来的事实标准,但是最近的变化使得setuptools的使用成为可选的。
这意味着像flit这样的项目可以使用新的pyproject。来指定一个不需要setup.py的不同构建系统。
话虽如此,在不久的将来,setuptools和配套的setup.py仍然是许多人的默认选择。
当你使用setup.py来分发你的包时,这里有一个推荐的工作流:
setup.py
- install_require关键字应该包含“正确运行所需的最少”的包。
Pipfile
- 表示包的具体需求
- 通过使用Pipenv安装包,从setup.py中获取最少需要的依赖项:
- 使用pipenv install '-e .'
- 这将导致你的Pipfile文件中生成如 "e1839a8" = {path = ".", editable = true} 所示的命令行
Pipfile.lock
- 从pipenv lock生成的可复制的环境的细节
澄清一下,将您的最低要求放在setup.py中,而不是直接使用pipenv install。然后使用pipenv install '-e .'命令将包安装为可编辑的。它将setup.py中的所有需求都获取到您的环境中。然后你可以使用pipenv lock来获得一个可复制的环境。
I don’t need to distribute my code as a package
太棒了!如果您正在开发的应用程序并不是要分发或安装的(个人网站、桌面应用程序、游戏或类似的东西),那么实际上并不需要setup.py。
在这种情况下,可以使用Pipfile/Pipfile.lock,根据前面描述的流来管理你的依赖项,以便在生产环境中部署可重复的环境。
I already have a requirements.txt
. How do I convert to a Pipfile
?
如果你运行pipenv安装,它应该会自动检测的requirements.txt,并将其转换为Pipfile,输出类似如下:
requirements.txt found, instead of Pipfile! Converting… Warning: Your Pipfile now contains pinned versions, if your requirements.txt did. We recommend updating your Pipfile to specify the "*" version, instead.
如果您在requirements.txt文件中指定了确切的版本,那么您可能希望将Pipfile更改为仅指定您真正需要的确切版本。这将使你获得真正过渡成requirements.txt文件的好处。例如,假设您有以下内容,但实际上并不需要numpy的精确版本:
[packages] numpy = "==1.14.1"
如果你没有任何特定的版本要求你的依赖,你可以使用通配符*告诉Pipenv,任何版本都可以安装:
[packages] numpy = "*"
如果您对允许使用*的任何版本感到紧张,那么最好指定大于或等于您已经使用的版本,这样您仍然可以利用新版本:
[packages] numpy = ">=1.14.1"
当然,保持最新的发布版本也意味着您要负责确保您的代码在包更改时仍能按预期工作。这意味着,如果您想要确保代码的功能发布,测试套件对于整个Pipenv流是必不可少的。
您允许包更新,然后运行测试,确保它们都通过,再锁定您的环境,然后您就可以放心了,因为您还没有引入破坏性的更改。如果确实因为依赖关系而出现问题,那么您需要编写一些回归测试,并可能对依赖关系的版本进行更多的限制。
例如,如果numpy==1.15是在运行pipenv install之后安装的,并且它破坏了您的代码(您希望在开发期间或测试期间注意到这一点),那么您有两个选项:
1.使用新版本的依赖项更新您的代码。
如果不可能向后兼容以前版本的依赖,你还需要在你的Pipfile中增加你需要的版本:
[packages] numpy = ">=1.15"
2.将Pipfile中依赖项的版本限制为<刚才破坏代码的版本:
[packages] numpy = ">=1.14.1,<1.15"
选项1是首选的,因为它确保您的代码使用最新的依赖项。但是,选项2花费的时间更少,而且不需要更改代码,只需限制依赖关系即可。
你也可以用pip使用的相同的-r参数p来安装需求文件:
pipenv install -r requirements.txt
如果您有一个dev-requirements.txt或类似的文件,您也可以将它们添加到Pipfile中。只要添加--dev参数,它就会被放到正确的部分:
pipenv install -r dev-requirements.txt --dev
另外,你也可以反向从Pipfile中生成需求文件requirements.txt:
pipenv lock -r > requirements.txt pipenv lock -r -d > dev-requirements.txt
What’s next?
在我看来,Python生态系统的一种自然发展是成为在从包索引(如PyPI)检索和构建包时使用Pipfile安装所需的最低依赖项的构建系统。需要再次注意的是,Pipfile设计规范仍在开发中,Pipenv只是一个参考实现。
也就是说,我可以预见将来setup.py中的install_require部分将不存在,而Pipfile将作为最低需求引用。或者,setup.py完全消失了,您以另一种方式获得元数据和其他信息,仍然使用Pipfile获得必要的依赖项。
Is Pipenv worth checking out?
肯定。即使它只是将您已经使用的工具(pip & virtualenv)合并到一个接口中。然而,它远远不止这些。通过添加Pipfile,您只需指定真正需要的依赖项。
您不再需要自己管理所有东西的版本,以确保能够复制开发环境。有了Pipfile.lock,你就可以安心地开发,因为你知道你可以在任何地方复制你的环境。
除此之外,Pipfile格式很可能会被像pip这样的官方Python工具所采用和支持,所以走在前面是很有好处的。哦,还要确保你把所有的代码都更新到Python 3: 2020年很快就到了。