开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天, 点击查看活动详情
一个项目最重要的就是跑起来, 大家基本会同时在本地开发多个项目, 而每个项目用到的环境都是不一样的, 如果这些项目都共用一份依赖那么会导致多个项目的依赖发生冲突以及导致线上服务不稳定,所以就需要用到虚拟环境隔离。在
Python
中提供了名为
venv
的虚拟环境管理包用于做多个项目的环境隔离,它提供了很多基础的功能,但是还有很多功能都需要开发者手动操作非常不方便,这时候就可以用到
Poetry
啦。
本文是
保障Python项目质量的工具
文章中
项目环境管理-Poetry
的拓展版,由于
Poetry
在1.2后发生了一些大变化,所以本文的内容不保证能支持
Poetry
1.2以下的版本。
1.最初的开始
在未依赖任何外部工具时,通常都会使用自带的工具来初始化项目的环境,如下:
➜ ~ cd demo
➜ demo
➜ demo python3 -m venv .venv
➜ demo source .venv/bin/activate
➜ demo python3 -m pip install --upgrade pip setuptools wheel
➜ demo python3 -m pip install -r requirements.dev.txt
这几个命令中,由venv
完成初始化和使用虚拟环境,再由pip
命令来安装包含测试环境的依赖。
这些工具都能正常的使用,但是却有几个弊端:
直接使用python3
命令,无法确定准确的Python
的版本,导致本地的Python
版本与其他人或者服务器的版本不同步。
每次都要显式的进入和切换虚拟换(Pycharm
会默认读取到虚拟环境)。
通过pip
来管理依赖,无法完成依赖管理,也无法自动的对依赖进行分组,如区分测试依赖和正式依赖等。
此外对于打包,推送包之类的功能还需要开发者去手动编辑文件再通过繁杂的命令去处理,这是非常麻烦的,而Poetry
对很多重复且需要开发者手动操作都步骤都统一起来,提供一些命令方便开发者去操作。
Poetry
的安装非常简单,一条命令就可以搞定了,不同系统的安装教程官方已经说得很详细,具体见官方安装文档
2.如何使用Poetry初始化项目环境
如果这是第一个项目,那么可以使用poetry new {项目名}
的命令来创建项目,如下:
➜ ~ poetry new demo
Created package demo in demo
这时Poetry
会在当前目录创建一个demo
目录,demo
目录里面的结构如下:
➜ ~ cd demo
➜ demo tree
├── demo
│ └── __init__.py
├── pyproject.toml
├── README.md
└── tests
└── __init__.py
2 directories, 4 files
可以看到Poetry
会帮忙创建一个最小项目的结构,其中demo
是我们本次要开发的目录,README.md
是一个项目描述文档,但它是空的,等着我们去填写,tests
则是一个测试用例的目录,而pyproject.toml
是Python
项目相关的一些配置,poetry
会预写一些项目的最小信息,如下:
[tool.poetry]
name = "demo"
version = "0.1.0"
description = ""
authors = ["so1n <qaz6803609@163.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.7"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
其中作者信息是通过git config
里面的配置获取的。
如果是在已有项目下使用Poetry
则可以通过poetry init
命令,这样Poetry
就会通过一个交互式命令行来协助开发者创建项目信息和虚拟环境,不过在创建之前要先确保系统上拥有自己想要的Python
版本,Poetry
是不会负责Python
版本的管理的,需要开发者通过手动处理,pyenv
,conda
等命令来完成这一步操作,具体的poetry init
操作如动图:
可以看到如果没有填写值得话,Poetry
会默认为你填写一些值,我在某些选项中填入了一些自己的值,最后pyproject.toml
会生成如下内容:
[tool.poetry]
name = "demo"
version = "0.0.1"
description = "My demo project"
authors = ["so1n <qaz6803609@163.com>"]
license = "Apache"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9.10"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
至此,项目初始化完毕,可以开始编写我们的项目了,但是当用PyCharm
打开项目的时候会发现有如图的提醒:
这个提醒是PyCharm
找不到Poetry
的可执行文件,需要通过我们指定Poetry
的路径才可以执行,这时候可以通过命令:
which poetry
来获取Poetry
的可执行路径并填写到弹窗里面,之后PyCharm
就会调用Poetry
进行虚拟环境初始化,并在当前路径创建.venv
文件夹,通过poetry env info
获取当前项目的虚拟环境:
➜ demo poetry env info
Virtualenv
Python: 3.9.10
Implementation: CPython
Path: /home/so1n/demo/.venv
Executable: /home/so1n/demo/.venv/bin/python
Valid: True
System
Platform: linux
OS: posix
Python: 3.9.10
Path: /usr/local
Executable: /usr/local/bin/python3.9
1.如果想在已有的项目下使用Poetry
则需要先删除当前的虚拟环境,并通过命令poetry env use 3.9.10
2.如果创建的虚拟环境版本不是自己想要的,且是通过pyenv
管理Python
版本,那么可以通过python-poetry.org/blog/announ…
3.如果第二点仍然无法解决问题,可以采用如下命令显示的指导Poetry
使用到正确的Python
版本:
pyenv local 3.9.10
poetry env use $(pyenv which python)
3.编写与运行项目
虚拟环境初始化完成后就可以开始编写项目了,这个demo
项目很简单,就是获取一个网站当前的状态,需要用到一个名为httpx
的包,这时可以通过命令:
poetry add httpx
来安装这个包,当命令执行完毕后,可以发现pyproject.toml
文件新增了一行关于httpx
包的版本描述:
[tool.poetry.dependencies]
python = "^3.9.10"
poetry = "^1.2.2"
httpx = "^0.23.1" # <------ 新增
这段描述意味着httpx
的版本会锁定在0.23.1
这个版本中。
安装好依赖后,在demo/__init__.py
编写如下代码:
import httpx
async def get_status_code() -> int:
async with httpx.AsyncClient() as client:
resp: httpx.Response = await client.get("https://so1n.me")
return resp.status_code
def main():
import asyncio
print(asyncio.run(get_status_code()))
if __name__ == "__main__":
main()
这段代码非常简单,就是请求https://so1n.me
并打印对应的HTTP状态码,接着可以在终端通过命令直接运行,不用再通激活虚拟环境,运行结果如下:
➜ demo poetry run python demo/__init__.py
不过这段命令比较长,经常这样输入会比较麻烦,这时可以采用Poetry
的脚本功能,只需要向pyproject.toml
文件追加如下内容:
[tool.poetry.scripts]
demo = 'demo.__init__:main'
这段内容中的demo = 'demo.__init__:main'
以等号分成两边,左边的demo
是脚本key,这意味着在tool.poetry.scripts
中不能出现相同的Key,而等号右边的demo.__init__:main
则代表要执行demo
目录下的__init__.py
的main
函数。
接下来可以通过poetry run {脚本key}
语法来执行我们想要跑的代码,执行效果如下:
➜ demo poetry run demo
除此之外,还可以通过poetry install
的方式来让Poetry
创建我们的demo
脚本:
➜ demo poetry install
Installing dependencies from lock file
No dependencies to install or update
Installing the current project: demo (0.0.1)
➜ demo poetry run which demo
/home/so1n/demo/.venv/bin/demo
➜ demo cat /home/so1n/demo/.venv/bin/demo
#!/home/so1n/demo/.venv/bin/python
import sys
from demo.__init__ import main
if __name__ == '__main__':
sys.exit(main())
可以看到Poetry
在/home/so1n/demo/.venv/bin/demo
路径下创建了一个脚本文件,如果这时候如果通过poetry shell
进入带有当前虚拟环境的交互shell
,则可以通过demo
直接执行我们的代码,如下:
➜ demo poetry shell
Spawning shell within /home/so1n/demo/.venv
➜ demo . /home/so1n/demo/.venv/bin/activate
(demo-py3.9.10) ➜ demo pwd
/home/so1n/demo/demo
(demo-py3.9.10) ➜ demo demo
4.运行测试用例
项目编写完成后需要确保我们的代码符合规范以及需要补充对应的测试用例,这是一个良好的习惯,在Python
生态中,常用的测试框架是pytest
,常用的检查代码规范则是通过pre-commit
去执行的。
可以通过保障Python项目质量的工具了解有什么提升代码质量的工具以及如何使用pre-commit
。
在poetry
可以通过如下方式安装pytest
, pytest-asyncio
和pre-commit
:
poetry add --group=dev pre-commit pytest pytest-asyncio
执行完命令后可以发现pyproject.toml
新增了如下内容:
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
pre-commit = "^2.20.0"
pytest-asyncio = "^0.20.2"
这块内容表示Poetry
托管的虚拟环境安装了pytest
和pre-commit
的依赖,但是他们是属于dev
组的,在正式情况下不会被使用,这对于导出依赖时非常有作用。
接下来先检查项目的代码格式,首先创建.pre-commit-config.yaml
文件,并输入如下内容
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.910
hooks:
- id: mypy
- repo: https://github.com/PyCQA/isort
rev: 5.9.3
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 21.7b0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
- id: flake8
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable', '--ignore-init-module-imports']
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: check-ast
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-json
- id: check-yaml
- id: debug-statements
- id: detect-private-key
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
然后执行如下命令:
➜ demo git init
➜ demo git:(master) ✗ poetry run pre-commit run --all-file
其中第一个命令是为项目进行git初始化,第二个命令是执行代码检查。
如果出现ModuleNotFoundError: No module named '_sqlite3'
错误,可以访问No module named _sqlite3了解如何解决。
执行完代码检查后,在tests
目录里面创建一个名为test_demo.py
文件,并输入如下内容:
import pytest
from demo import get_status_code
pytestmark = pytest.mark.asyncio
class TestDemo:
async def test_demo(self):
assert 200 == await get_status_code()
接着运行poetry run pytest --capture=no -v
执行测试结果:
➜ demo git:(master) ✗ poetry run pytest --capture=no -v
============================ test session starts =============================
platform linux -- Python 3.9.10, pytest-7.2.0, pluggy-1.0.0 -- /home/so1n/demo/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/so1n/demo
plugins: anyio-3.6.2, asyncio-0.20.2
asyncio: mode=strict
collected 1 item
tests/test_demo.py::TestDemo::test_demo PASSED
============================= 1 passed in 0.32s ==============================
发现测试正常,接下来可以生成项目的依赖,使其他没有使用Poetry
的环境也可以读取到项目的依赖,对应的命令如下:
poetry export -o requirements.txt --without-hashes --with-credentials
poetry export -o requirements-dev.txt --without-hashes --with-credentials --with dev
其中第一条命令是生成正常使用下的依赖,第二条命令是包含测试环境的依赖。
5.打包与发布
如果这个项目想发到PyPi
供别人使用,那么可以先打包项目再发布到PyPi
中,这时候需要先修改pyproject.toml
中version
的值,比如把它改为0.0.2
,再通过poetry builld
和poetry publish
命令发布。
当然,除了发布到PyPi
外,还可能把项目的代码发布到Github
中,同时为了让开发者快速的找到对应的版本,往往会给当前的commit
打上对应的tag,这就代表着我们必须保证version
中的值与tag
是一致的,此外,项目中demo/__init__.py
也需要添加如下内容:
__version__ = "0.0.2"
使得别人在引用这个项目时知道项目的版本号是多少,这样一来每次做项目升级的时候需要同时修改三处地方的版本号,非常折腾,为了省心省力,可以采用Poetry
的一个插件--poetry-dynamic-versioning来解决这个问题。
首先是通过命令:
poetry self add "poetry-dynamic-versioning[plugin]"
向Poetry
安装插件,然后向pyproject.toml
添加如下内容:
[tool.poetry-dynamic-versioning]
enable = true # 代表启用该插件
metadata=false # 生成的版本号不带上其他数据
vcs = "git" # 指定的版本控制系统为git
format = "v{base}" # 指定版本号的生成规则
接着把pyproject.toml
文件中的build-system
块替换为如下内容:
[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry_dynamic_versioning.backend"
最后把pyproject.toml
和demo/__init__.py
中的版本号改为"0.0.0"
(这是poetry-dynamic-versioning
的一个动态版本号占位符)
一切准备就绪后可以执行如下命令进行代码提交以及打上对应的版本tag:
➜ demo git:(master) ✗ git add *
➜ demo git:(master) ✗ git commit -m"Add, First commit"
➜ demo git:(master) ✗ git tag v0.0.2
接下来执行poetry build
对项目打包:
➜ demo git:(master) ✗ poetry build
Building demo (v0.0.2)
- Building sdist
- Built demo-0.0.2.tar.gz
- Building wheel
- Built demo-0.0.2-py3-none-any.whl
通过命令可以发现包的版本正好是我们提交的tagv0.0.2
中的0.0.2
,如果找到包里面的demo/__init__.py
的文件,可以发现文件中存在如下一行代码:
__version__ = "v0.0.2"
其中__version__
的值与tag
一样。
打包完成后就可以把包传到PyPi
了,不过在第一次上传之前需要通过如下命令配置自己的PyPi
账户信息:
# my-token可以在`pypi.org`网站中设置
poetry config pypi-token.pypi my-token
# 或者通过如下命令配置自己的账号密码
poetry config http-basic.pypi <username> <password>
最后通过poetry publish
命令即可把包传到PyPi
中
6.Poetry的pre-commit
从上面的流程可以发现Poetry
为我们带来方便的虚拟环境管理和依赖管理,但有些时候仍然需要我们调用poetry update
来确定依赖有及时更新以及通过poetry export
来导出依赖,这时可以通过pre-commit-config
来确保提交代码的时候能执行自动执行poetry update
和poetry export
等语法。
第一步先向.pre-commit-config.yaml
追加如下内容:
- repo: https://github.com/python-poetry/poetry
rev: '' # add version here
hooks:
- id: poetry-check
- id: poetry-lock
- id: poetry-export
args: [ "-f", "requirements.txt", "-o", "requirements.txt", "--without-hashes", "--with-credentials"]
- id: poetry-export
args: [ "-f", "requirements.txt", "-o", "requirements-dev.txt", "--without-hashes", "--with-credentials", "--with", "dev"]
并执行如下命令:
poetry run pre-commit install
poetry run pre-commit run --all-file
验证pre-commit
是否正常执行。
至此就可以享受Poetry
为我们带来的便利,只要简单几部操作就可以完成虚拟环境的使用和依赖管理,但是本文还有一些内容没有介绍,可以通过官方文档了解Poetry
的更多功能。