from Pixabayfrom Pixabay

再見了 pip!最佳 Python 套件管理器——Poetry 完全入門指南〉發表至今,已過了 1 年多,這意味我也用了一年多的 Poetry。

感覺如何?——我覺得還不錯!可見不是三分鐘熱度而已。

對現在的我而言,Poetry 已成為專案開發不可或缺的元素。它不僅提供了更加便捷的專案套件管理和版本控制,同時,Poetry 支援 pyproject.toml 作為設定檔的特性,也使得我更容易使用其他也採用 pyproject.toml 的工具。

相關文章:pyproject.toml 介紹 + VS Code 整合 Black、yapf、isort 教學

系列:Python Poetry 三部曲

  1. 再見了 pip!最佳 Python 套件管理器——Poetry 完全入門指南
  2. Poetry + pyenv 實戰心得:常用指令與注意事項
  3. 終結 requirements.txt:Dockerfile 多階段建構 Poetry 虛擬環境(待發表)

本文主旨

本文將補充系列第一篇中在「情境使用」方面的不足之處,尤其是針對 Poetry 和 pyenv 同時使用時可能出現的問題進行討論。這是第一篇所遺漏的內容。

透過本文,我希望能夠提供更全面、更實用的 Poetry 使用建議,讓讀者在使用 Poetry 和 pyenv 時能夠充分發揮它們的優勢,並減少不必要的困惑。

以 poetry-demo 為例

poetry-demo 是本文作為例示的專案模版,但我們不會安裝太多套件,僅各取一個作為示範之用。

有一個簡單的具體實例,還是比抽象的描述容易理解得多。

我們會從一台全新的 Linux VM(Ubuntu 20.04)開始,先安裝 pyenv,再安裝 Poetry,然後再一起使用它們,建立 Python 專案與虛擬環境。

使用的 Poetry 版本:1.5.1

Poetry 在 1.2 版後,對於部分指令進行有較大的改動與擴張,導致舊指令使用上不完全相容,這次會採用當前的最新版——1.5.1——來進行示範。

使用的 pyenv 版本:2.3.18

pyenv 在 v2.3.0 以後,已經大幅簡化了設定操作,所以這裡也有必要強調一下使用的版本:v2.3.18

相關文章也有就新版設定內容進行更新,可參考〈Ubuntu 安裝使用 pyenv + pyenv-virtualenv〉。

安裝 Poetry、pyenv、Python 3.10

請直接參考〈Linux Python 開發環境設定:zsh、zinit、pyenv、poetry、docker〉中的「三、設定 pyenv」、「四、設定 Poetry」部分。

不只是安裝,還包括設定 PATH 等環節,這些步驟都是必要的。完成這些步驟後,我們就擁有了 Poetry 和 pyenv。

透過 pyenv 安裝 Python 3.10.11

1
pyenv install 3.10.11

pyenv 是為了方便我們管理多個 Python 版本,下面我們會探討不同專案分別使用多個 Python 版本時的 Poetry + pyenv 操作注意事項。

這裡至少要先有一個 Python 版本,才能順利安裝我們的專案,在此以 Python 3.10.11 為例。

Poetry 與 pyenv 的部分功能重疊

安裝完後,是否要設定pyenv local 3.10.11pyenv global 3.10.11,取決於你是否有「多專案且多種 Python 版本」需求。

如果只需要一種 Python 版本,那將其設定為global已足:

1
pyenv global 3.10.11

前述文章所言:

因為 Poetry 自帶了虛擬環境管理功能,容易和 pyenv-virtualenv 疊床架屋,徒增管理上的混淆,所以我現在一律只使用 Poetry + venv 來管理 Python 虛擬環境

即使在不同專案需要多版本 Python 情況下,pyenv-virtualenv 也不是必須。只要善用pyenv localpoetry env use兩大指令即可。

綜上所述,這也是為什麼我認為 Poetry 的教學應該涵蓋對 pyenv 的整合,因為在「虛擬環境管理」方面,兩者的功能有一定重疊。


前置作業總算大功告成,我們開始建立 poetry-demo 吧!

一、初始化 Poetry 專案

先確認一下當前的 Poetry 版本,使用poetry --version

1
2
❯ poetry --version
Poetry (version 1.4.2)

我的 Poetry 是一段時間前安裝的 1.4.2,需要更新一下,以符合本文使用的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ poetry self update

Using version ^1.5.1 for poetry

Updating dependencies
Resolving dependencies... (2.6s)

Writing lock file

Package operations: 0 installs, 5 updates, 0 removals

• Updating platformdirs (2.6.2 -> 3.5.1)
• Updating poetry-core (1.5.2 -> 1.6.1)
• Updating poetry-plugin-export (1.3.1 -> 1.4.0)
• Updating virtualenv (20.21.1 -> 20.23.0)
• Updating poetry (1.4.2 -> 1.5.1)

因為是使用全域安裝 Poetry,上面的套件更新訊息和專案的虛擬環境無關——畢竟我們根本都還沒有為專案建立專屬的 Python 虛擬環境!

也可以指定要升級的版本:

1
poetry self update 1.5.1

使用poetry init初始化專案

確認完 Poetry 版本,開始建立專案:

1
2
3
mkdir poetry-demo
cd poetry-demo
poetry init

poetry init會出現下列互動式訊息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
This command will guide you through creating your pyproject.toml config.

Package name [poetry-demo]:
Version [0.1.0]:
Description []:
Author [kyo <odinxp@gmail.com>, n to skip]:
License []:
Compatible Python versions [^3.8]:

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file

[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["kyo <odinxp@gmail.com>"]
readme = "README.md"
packages = [{include = "poetry_demo"}]

[tool.poetry.dependencies]
python = "^3.8"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Do you confirm generation? (yes/no) [yes]

中間的「Would you like to define your main/development dependencies interactively?」兩個問句,我都回答「no」,最後一個則是「yes」。

初始化後的pyproject.toml內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["kyo <odinxp@gmail.com>"]
readme = "README.md"
packages = [{include = "poetry_demo"}]

[tool.poetry.dependencies]
python = "^3.8"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

可以看到其中有一行「readme = "README.md"」,此時專案必須有README.md,否則會找不到檔案。我都直接刪除該行,比較省事。

使用poetry new快速初始化

要達到專案初始化的效果,你也可以直接用poetry new poetry-demo指令,更快速!不過它也會幫你做「更多事」,細節請參考文件

二、為專案建立 Python 虛擬環境

這裡是「重頭戲」之一,但做法上並不是那麼「直觀」,常常容易讓人混淆。僅使用 pyenv 的前提下,再安裝pyenv-virtualenv來建立虛擬環境,確實不難。

但現在有了 Poetry,兩者的搭配使用方式就很重要,這也是為什麼我一再強調,有了 Poetry,乾脆就不要再裝pyenv-virtualenv了。

使用 Poetry 建立虛擬環境

在第一篇文章中,雖然我提過poetry shell有時候可以替代poetry env use,作為快速建立虛擬環境的便捷手段。

但是,當你還沒有為專案建立虛擬環境,且作業系統中包含了不止一個 Python 版本時,建議就不要用poetry shell來建立虛擬環境——因為它很可能會選擇不是你要的 Python 版本。

儘管我們使用 pyenv 來管理 Python,但完整的 Linux 發行版往往都自帶了系統的 Python。比如我的 Ubuntu 就自帶了 3.8.x,這正是為何上面pyproject.toml會有一行「python = "^3.8"」而不是^3.10——因為 Poetry 偵測到的是系統預設的 Python,而不是 pyenv 的 Poetry。

換句話說,無論透過 pyenv 安裝了幾個 Python 版本,這些資訊對 Poetry 而言,仍可能是陌生的。

為了讓 Poetry 在建立虛擬環境時,能確實使用你想要的 Python 版本,我們必須善用poetry env use指令才行。

三、確定專案使用的 Python 版本

第一篇文章中也提到:

我覺得學習 Poetry 的第一道關卡,就是它對於虛擬環境的管理。

現在看來一點也沒錯!

指定虛擬環境 Python 版本的標準做法

在使用 pyenv 的情況下,Poetry 官方文件有補充一個讓你能「確定」虛擬環境會使用的 Python 版本的做法:

If you use a tool like pyenv to manage different Python versions, you can set the experimental virtualenvs.prefer-active-python option to true. Poetry will then try to find the current python of your shell.

其中的「experimental」表示這是一個實驗性功能。所以我不偏好這個做法。

For instance, if your project requires a newer Python than is available with your system, a standard workflow would be:

1
2
3
pyenv install 3.9.8
pyenv local 3.9.8 # Activate Python 3.9 for the current project
poetry install

主要分成兩個步驟:

  1. virtualenvs.prefer-active-python設為true
  2. 使用 pyenv 的pyenv local指令。

我偏好的做法:poetry env use

如果你只確定需要「一個」Python 版本,且已經將其設定為global,那麼前述的virtualenvs.prefer-active-python設定,或可省略。

poetry env use有下列幾種用法:

1
poetry env use /full/path/to/python
1
poetry env use python
1
poetry env use python3.7
1
poetry env use 3.7

後三者的pythonpython3.73.7,都和你的PATH有關。

換句話說,如果你在終端機打python3.7,有成功進入「Python 互動式視窗」,那就表示這個版本的 Python 確實存在PATH中。

使用which指令確認 Python 版本是否存在PATH

不想進入 REPL,只想確認 Python 版本是否存在PATH中,可以使用which指令:

1
2
3
4
5
6
which python3.9
/home/kyo/.pyenv/shims/python3.9
which python3.10
/home/kyo/.pyenv/shims/python3.10
which python
/home/kyo/.pyenv/shims/python

聰明的你應該猜到了,我們只要確保 Python 版本已存在於PATH,透過poetry env use <指定的python版本>即可確定專案使用的 Python 版本。

不過,這個<指定的python版本>必須要先透過 pyenv 安裝好,而且你通常要將其設定為globallocal,系統才找得到。

poetry-demo 操作

回到案例,這裡我們已經將 3.10.11 設為global。所以輸入python3.10指令時,會進入互動式視窗。

此時只要使用下列指令,基本上可以確定使用的 Python 版本:

1
poetry env use 3.10.11

因為已經有設定global,單純使用poetry env use python應該也是可以成功套用 3.10。但保險起見,使用指令時還是建議輸入完整的版本號,包括尾綴的.11

四、不同專案使用不同 Python 版本

即使有「多專案多 Python 版本」需求,也未必要變更前述的virtualenvs.prefer-active-python設定。

透過pyenv local+poetry env use,可以為不同專案設定不同的 Python 版本。

假設你有 a、b、c 三個專案,分別要使用 Python 3.7.11、3.9.12、3.10.11,依照前段介紹,我們可以這麼做。

首先,pyenv versions確認這三個版本的 Python 都已經由 pyenv 安裝完成:

1
2
3
4
5
❯ pyenv versions
system
3.7.11
3.9.12
* 3.10.11 (set by /home/kyo/.pyenv/version)

接下來就很簡單了,為各專案設定好pyenv local(好讓PATH可以成功找到對應的 Python 執行檔),然後再poetry env use <指定的python版本>

假設 b 專案要使用 3.9.12,則做法如下:

1
2
3
cd b
pyenv local 3.9.12
poetry env use 3.9.12

其餘專案以此類推。

五、如何移除 Poetry 虛擬環境?

參考文件,標準做法如下:

1
2
3
4
poetry env remove /full/path/to/python
poetry env remove python3.7
poetry env remove 3.7
poetry env remove test-O3eWbxRl-py3.7

然而,因為我們已經將virtualenvs.in-project改設為true,也就是直接在專案中建立名為.venv的虛擬環境。

上述的指令基本都沒有作用了。

但我就真的需要砍掉重練啊!怎麼辦?

兩個方法

此時還有兩個簡單的方法可用。

方法一,就是直接砍掉.venv,簡單有效!

1
rm -rf .venv

方法二,我們依舊可以使用下列指令,優雅地移除它:

1
2
❯ poetry env remove --all
Deleted virtualenv: /home/kyo/poetry-demo/.venv

光專案初始化與虛擬環境管理就用掉了 5 個 h2 標題,可見其複雜。現在,我們進入第二部分——套件的安裝與管理。

六、安裝套件至 main dependencies

使用poetry add指令。

參考文件,可以發現add指令的用法還挺多元的!

我覺得對一般使用者而言,poetry add有兩個重點:

  1. 了解poetry add的「多階段行為」。
  2. 了解--group參數用法。

重點一:poetry add多階段行為

如上篇文章所言,poetry add實際上會做 3 件事,依序為:

  1. 更新pyproject.toml
  2. 依照pyproject.toml的內容,更新poetry.lock。(相當於poetry lock
  3. 依照poetry.lock的內容,更新虛擬環境。(相當於poetry install

為什麼知道這個很重要?

因為當你不是使用poetry add指令,而是直接修改pyproject.toml時,此時上述的第 2、3 步都不會自動執行

但通常你手動修改 toml 檔最終都是為了變更虛擬環境,所以更新完pyproject.toml後,我們還要再使用poetry lockpoetry install指令才行!

對於不熟悉上述流程的初學者,很容易遺漏,並感到困惑。

重點二:--group

舊版(1.1.x)只有 main 和 dev 兩種依賴環境設定,新版(1.2.0)增加了--group參數,讓你可以除了 main 和 dev 外,還有能自訂多種 group,增加使用上的彈性。

比如可以命名不同的群組如下:

  • test
  • dev
  • prod

基本語法(後續還會提及):

1
poetry add pytest --group dev

在新版(1.2)的pyproject.toml中會如此記載:

1
2
3
[tool.poetry.group.dev.dependencies]
pytest = "^6.0.0"
pytest-mock = "*"

而舊版則是:

1
2
3
4
# Poetry pre-1.2.x style, understood by Poetry 1.0–1.2
[tool.poetry.dev-dependencies]
pytest = "^6.0.0"
pytest-mock = "*"

兩者的差異,是版本過渡時要特別注意的。

雖然彈性變大,但我個人目前還是只有使用 main 和 dev 而已。

poetry-demo 操作

至此,我們也安裝 Django 3.2.x 至 main dependencies 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ poetry add django@^3.2

Updating dependencies
Resolving dependencies... (0.5s)

Package operations: 5 installs, 0 updates, 0 removals

• Installing typing-extensions (4.6.3)
• Installing asgiref (3.7.2)
• Installing pytz (2023.3)
• Installing sqlparse (0.4.4)
• Installing django (3.2.19)

Writing lock file

這個@符號(運算子)要怎麼用,請參考文件

七、安裝套件至 dev dependencies

上篇文章中,我們已經探討過「明確區分開發環境專用的套件」的重要性。

舊版的指令是這樣的,以black為例:

1
2
3
poetry add black --dev
# 或
poetry add black -D

然而--dev (-D)在新版已棄用

--dev (-D): Add package as development dependency. (Deprecated, use -G dev instead)

因為加入了 group 機制,新版的指令略有不同:

1
2
3
poetry add black --group dev
# 或
poetry add black -G dev

講白了就是變囉嗦了一點。

此時的 toml 檔內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["kyo <odinxp@gmail.com>"]
packages = [{include = "poetry_demo"}]

[tool.poetry.dependencies]
python = "^3.8"
django = "^3.2"

[tool.poetry.group.dev.dependencies]
black = "^23.3.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

八、poetry install --sync

不久前才發現,虛擬環境用久了,安裝的套件似乎和 lock 檔不完全一致!我一直以為兩者是一定同步的🐸,顯然不是。

參考文件,可用下列指令確保同步:

1
poetry install --sync

九、Docker 環境中使用 Poetry

前文中有這麼一段,闡述我不在 Docker 中使用 Poetry 的理由替代方案

所幸 Poetry 依舊可以輸出requirements.txt,Docker 部署環境就繼續使用這個舊方案即可,而且 Poetry 本來主要就是用於「開發」時的套件管理,對部署差別不大。

說是這樣說,但一年多用下來,我發現這個做法也不盡理想,它至少存在兩個問題:

  1. 套件有變動時,常常會忘記匯出requirements.txt:你可以說這是人的問題,但這個 export requirements.txt做法,就真的很容易讓人忘記。
  2. 由 Poetry 匯出的requirements.txt,不一定能透過 pip 正常安裝套件——兩者存在輕微的相容性問題

怎麼解?我在原文也已經補充了:

使用 multi-stage builds 的 Dockerfile,可以在第一階段安裝 Poetry,第二階段再把 Poetry 捨棄,這樣就不會有多餘的耦合與依賴了。日後會專文介紹。

對,所以系列的第三篇會把這部分補完。