from Pixabayfrom Pixabay

2024/04/26:重新編輯全文,新增、刪除若干段落,提升文字清晰與流暢度。
2024/01/09:刪除部分內容,使文章更緊湊、好讀。

Python 套件管理器——Poetry 完全入門指南〉發表至今,已過了 2 年,這意味著我使用 Poetry 達 2 年了。

對我而言,Poetry 已是 Python 專案開發不可或缺的要素。

它不僅提供了更加便捷的套件管理版本控制。而且,Poetry 以 pyproject.toml 作為設定檔,讓我可以同時透過 pyproject.toml 管理其它工具,比如 Mypy、Ruff 等。

相關文章:Python 開發:pyproject.toml 介紹 + 使用教學

這是我喜歡的方式。

系列:Python Poetry 三部曲

  1. Python 套件管理器——Poetry 完全入門指南
  2. Poetry + pyenv 教學:常用指令與注意事項
  3. Docker 教學:Dockerfile 多階段建構 Poetry 虛擬環境(待發表)

本文主旨

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

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

範例與環境介紹

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

有一個簡單的具體實例,比單純描述要容易理解。

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

Poetry 版本:1.5.1

Poetry 在 1.2 版後,對於部分指令進行有較大的改動,導致舊有的指令不完全相容,這次會採用當前(2023 年 6 月)的最新版——1.5.1——來進行示範。

pyenv 版本:2.3.18

pyenv 在 v2.3.0 以後,已經大幅簡化了設定操作。本文使用版本:v2.3.18

相關文章也有就新版設定內容進行更新,可參考:


安裝 Poetry、pyenv

請直接參考〈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 版本,才能順利安裝我們的專案,在此以 3.10.11 為例。

Poetry 與 pyenv 的整合

安裝完後,是否要設定pyenv local 3.10.11pyenv global 3.10.11,取決於你是否有「多專案、多 Python 版本」需求——通常一定會有☺️

設定 pyenv global 與 local

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

1
pyenv global 3.10.11

但你也可以同時設定兩者,這樣在特定專案中會使用特定 Python 版本,而在不指定時則使用global設定的 Python 版本。

2024/06/21補充:Poetry 有時就是不鳥你的pyenv global設定,而直接使用作業系統的預設 Python 版本。所以最好還是用pyenv local

比如幫某個舊專案設定為 3.8.12:(要先cd至該專案目錄)

1
pyenv local 3.8.12

此時專案中會新增一個.python-version檔案,內容就是你設定的 Python 版本:

1
3.8.12

建議:不要使用 pyenv-virtualenv

如前文所言:

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

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

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

退萬步言,當你的 Python 專案愈來愈多的時候,你就會深深感受到「一個專案對應一個虛擬環境」(而且最好直接放在專案裡)的必要性

小結:最佳實踐

所以,不要麻煩了!

直接使用 Poetry 的虛擬環境管理功能,並將virtualenvs.in-project設為true,就是我認為的最佳實踐

什麼,你還沒打開?沒關係,給你指令:

1
poetry config virtualenvs.in-project true

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

一、初始化 Poetry 專案

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

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

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

1
poetry self update

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

1
poetry self update 1.5.1

使用poetry init初始化專案

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

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

poetry init後會出現互動式訊息,上一篇已有詳細介紹

初始化後的pyproject.toml內容:

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

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

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

比起一開始使用的 v1.19,新版多了這行:

1
readme = "README.md"

我習慣——直接刪除

因為沒有README.md,反而會造成錯誤。如果有就可以保留。


二、為專案建立 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 版本

在有多個 Python 版本的情況下,想讓 Poetry 使用特定的 Python 版本,有時真的不是那麼容易😂

指定虛擬環境 Python 版本的官方做法(experimental)

在使用 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」表示這是一個實驗性功能,我不偏好這個做法,在此省略。有興趣的讀者可以自行參考文件。

我偏好的做法:poetry env use

將想用的 Python 版本設定為globallocal後,使用poetry env use指令來確保專案虛擬環境的 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指令時,會進入互動式視窗。

1
2
3
Python 3.10.11 (main, Apr  7 2023, 16:41:32) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

此時只要使用下列指令建立 Poetry 虛擬環境,基本上可以確定,使用的 Python 版本為 3.10.11:

1
poetry env use 3.10.11

保險起見,使用指令時還是建議輸入完整的版本號,例如3.10.11,而不是3.10。尤其是當你的 pyenv 中有多個 3.10.x 版本時,這樣可以避免混淆。


四、不同專案使用不同 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

此時我們再次體會到,「虛擬環境就是一個資料夾」的真理,以及把虛擬環境放在專案目錄下的好處。

方法二:使用poetry env remove --all

使用下列指令,優雅地移除它:

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

光是專案初始化虛擬環境管理就用掉大半篇幅,可見其複雜。現在我們進入第二部分——套件的安裝與管理。

六、安裝套件至 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 而已,比如:

1
poetry add black --group 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 捨棄,這樣就不會有多餘的耦合與依賴了。日後會專文介紹。

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