1030 lines
51 KiB
Markdown
1030 lines
51 KiB
Markdown
來源:[再見了 pip!最佳 Python 套件管理器——Poetry 完全入門指南](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8)
|
||
|
||
---
|
||
|
||
# 再見了 pip!最佳 Python 套件管理器——Poetry 完全入門指南
|
||
|
||
|
||
[]
|
||
|
||
前陣子工作上的專案從原先的 pip 改用 Poetry 管理 Python 套件,由於採用 Poetry 正是我的提議,所以得身先士卒,研究 Poetry 使用上的重點與學習成本,並評估是否真有所值——講白了就是至少要利大於弊,不然會徒增團隊適應上的負擔。
|
||
|
||
拜這個機會所賜,我對 Poetry 總算有了一個較為全面的理解。
|
||
|
||
習慣後,現在我所有的個人開發也都改用 Poetry 來管理套件及虛擬環境,對於 Poetry 這個略嫌複雜的工具(相較 pip),上手的同時我也感受到它確實存在一些**學習門檻**,間接促使了本文的誕生。
|
||
|
||
### 本文定位:獻給 Poetry 新手的使用說明書
|
||
|
||
有鑑於 Poetry 真的有點複雜,如果要推薦別人使用,我想還是有必要好好介紹一下。換句話說,這會是一篇**完整的入門教學**。
|
||
|
||
本文除了講解如何使用 Poetry,還會先**不厭其煩地闡述它所解決的痛點**,如果對此興趣有限,可以直接跳到「[從零開始使用 Poetry](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%BE%9E%E9%9B%B6%E9%96%8B%E5%A7%8B%E4%BD%BF%E7%94%A8-Poetry)」章節,但看完前導部分,相信能更加體會 Poetry 的**必要性與強大**之處。
|
||
|
||
為了讓你**無痛上手**,這將會是一篇超過 8000 字的長文,還請多多擔待。🙏
|
||
|
||
### 本文目錄
|
||
|
||
方便快速跳轉到有興趣的部分,桌面版用戶可和右下角的「回到最上方」搭配使用:
|
||
|
||
1. [Poetry 是什麼?](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#Poetry-%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F)
|
||
2. [名詞解釋:虛擬環境管理、套件管理、相依性管理](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%90%8D%E8%A9%9E%E8%A7%A3%E9%87%8B%EF%BC%9A%E8%99%9B%E6%93%AC%E7%92%B0%E5%A2%83%E7%AE%A1%E7%90%86%E3%80%81%E5%A5%97%E4%BB%B6%E7%AE%A1%E7%90%86%E3%80%81%E7%9B%B8%E4%BE%9D%E6%80%A7%E7%AE%A1%E7%90%86)
|
||
3. [pip 的最大不足](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#pip-%E7%9A%84%E6%9C%80%E5%A4%A7%E4%B8%8D%E8%B6%B3)
|
||
4. [pip 替代方案選擇——Pipenv vs Poetry](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#pip-%E6%9B%BF%E4%BB%A3%E6%96%B9%E6%A1%88%E9%81%B8%E6%93%87%E2%80%94%E2%80%94Pipenv-vs-Poetry)
|
||
5. [選擇 Poetry 的兩個理由](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E9%81%B8%E6%93%87-Poetry-%E7%9A%84%E5%85%A9%E5%80%8B%E7%90%86%E7%94%B1)
|
||
6. [從零開始使用 Poetry](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%BE%9E%E9%9B%B6%E9%96%8B%E5%A7%8B%E4%BD%BF%E7%94%A8-Poetry)
|
||
7. [安裝 Poetry](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%AE%89%E8%A3%9D-Poetry)
|
||
8. [初始化 Poetry 專案](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%88%9D%E5%A7%8B%E5%8C%96-Poetry-%E5%B0%88%E6%A1%88)
|
||
9. [管理 Poetry 虛擬環境](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E7%AE%A1%E7%90%86-Poetry-%E8%99%9B%E6%93%AC%E7%92%B0%E5%A2%83)
|
||
10. [Poetry 常用指令](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#Poetry-%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4)
|
||
11. [Poetry 常見使用情境與操作 QA](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#Poetry-%E5%B8%B8%E8%A6%8B%E4%BD%BF%E7%94%A8%E6%83%85%E5%A2%83%E8%88%87%E6%93%8D%E4%BD%9C-QA)
|
||
12. [結語:井然有序的複雜](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E7%B5%90%E8%AA%9E%EF%BC%9A%E4%BA%95%E7%84%B6%E6%9C%89%E5%BA%8F%E7%9A%84%E8%A4%87%E9%9B%9C)
|
||
|
||
---
|
||
|
||
## Poetry 是什麼?
|
||
|
||
要了解 Poetry 大致的作用與功能,參考 [Poetry GitHub](https://github.com/python-poetry/poetry#poetry-dependency-management-for-python) 說明是一個不錯的開始:
|
||
|
||
> **Poetry: Dependency Management for Python**
|
||
> Poetry helps you declare, manage and install dependencies of Python projects
|
||
|
||
而 [Poetry 官網](https://python-poetry.org/)的 slogan 則更加簡潔有力:
|
||
|
||

|
||
|
||
簡單來說,**Poetry 類似 pip,能協助你進行套件管理(dependency management),但又比 pip 強大得多,因為它還包含了 pip 所未有的下列功能:**
|
||
|
||
- 虛擬環境管理
|
||
- 套件相依性管理
|
||
- 套件的打包與發布
|
||
|
||
其中**最為關鍵**的是「**套件的相依性管理**」,也是本文的重點,而「套件的打包與發布」與主題較無關係,所以不會提及。
|
||
|
||
## 名詞解釋:虛擬環境管理、套件管理、相依性管理
|
||
|
||
開始前,要先大致說明標題中這三者的區別,才不易混淆文中的內容。這裡的定義可能不盡準確,但至少對理解文中的表達能有所幫助。
|
||
|
||
### 虛擬環境管理
|
||
|
||
指的是使用內建的 venv 或 virtualenv 套件來建立及管理 Python 的虛擬環境,不同的虛擬環境間各自獨立,也就是對應的路徑各不相同。
|
||
|
||
### 套件管理、依賴管理(dependency management)
|
||
|
||
指的是使用 pip 這類的套件管理器來管理 Python 環境(未必是虛擬環境),即管理環境中所安裝的全部套件(package、dependency)及其版本。
|
||
|
||
在這個**語境**下,dependency 基本上就是指你安裝的 package。
|
||
|
||
### 「套件的」相依性管理、依賴解析
|
||
|
||
這個有點難定義,它並不是一個非常通俗且有共識的名詞,我在英文中也還難找到對應的單字。本文使用它時,主要指的是**套件與套件之間的依賴關係及版本衝突管理**,也就是套件的「**相依性管理**」。在下文提及的 Podcast 中,又稱為「**依賴解析**」。
|
||
|
||
所謂套件的「**版本衝突**」指的是單一套件被兩個以上的套件所依賴,但不同的套件對依賴的套件有著不同的**最低或最高版本要求**,若兩者要求的範圍「**沒有交集**」,則會發生衝突而導致套件**失效**或**無法安裝**。
|
||
|
||
## pip 的最大不足
|
||
|
||
大概在 2 年前就聽過 Poetry 的大名,不過那時我還沒有套件相依性管理的強烈需求,加上看起來需要一些學習成本(確實如此),所以就一直擱在一旁,直到真正體會到了 pip 的不足。
|
||
|
||
pip 是 Python 內建的套件管理工具,而它的最大罩門,就是對於「套件間的相依性管理」能力不足。尤其是在「**移除**」套件時的依賴解析——可以說沒有。這也是我提議改用 Poetry 的根本原因。
|
||
|
||
怎麼說?看完下面的例子就能明白。
|
||
|
||
### `pip uninstall`的困境:以 Flask 為例
|
||
|
||
假設現在你的工作專案中有開發 API 的需求,經過一番研究與討論,決定使用 [Flask](https://github.com/pallets/flask) 網頁框架來進行開發。
|
||
|
||
我們知道,很多套件都有依賴的套件,也就是使用「別人已經造好的輪子」來構成套件功能的一部分。
|
||
|
||
安裝主套件時,這些依賴套件也**必須一併安裝,主套件才能正常運作**,這裡的 Flask 就是如此。安裝 Flask 時,不會只安裝單一個`flask`套件,還會安裝所有 Flask 的必要構成部分——也就是依賴套件,結果如下:
|
||
|
||
```
|
||
❯ pip install flask
|
||
Collecting flask
|
||
Downloading Flask-2.1.1-py3-none-any.whl (95 kB)
|
||
|████████████████████████████████| 95 kB 993 kB/s
|
||
Collecting importlib-metadata>=3.6.0
|
||
Using cached importlib_metadata-4.11.3-py3-none-any.whl (18 kB)
|
||
Collecting itsdangerous>=2.0
|
||
Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
|
||
Collecting Werkzeug>=2.0
|
||
Downloading Werkzeug-2.1.1-py3-none-any.whl (224 kB)
|
||
|████████████████████████████████| 224 kB 2.8 MB/s
|
||
Collecting click>=8.0
|
||
Downloading click-8.1.2-py3-none-any.whl (96 kB)
|
||
|████████████████████████████████| 96 kB 1.9 MB/s
|
||
Collecting Jinja2>=3.0
|
||
Downloading Jinja2-3.1.1-py3-none-any.whl (132 kB)
|
||
|████████████████████████████████| 132 kB 3.7 MB/s
|
||
Collecting zipp>=0.5
|
||
Using cached zipp-3.7.0-py3-none-any.whl (5.3 kB)
|
||
Collecting MarkupSafe>=2.0
|
||
Downloading MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl (13 kB)
|
||
Installing collected packages: zipp, MarkupSafe, Werkzeug, Jinja2, itsdangerous, importlib-metadata, click, flask
|
||
Successfully installed Jinja2-3.1.1 MarkupSafe-2.1.1 Werkzeug-2.1.1 click-8.1.2 flask-2.1.1 importlib-metadata-4.11.3 itsdangerous-2.1.2 zipp-3.7.0
|
||
```
|
||
|
||
從上可知,`pip install flask`還會一併安裝`importlib-metadata`、`itsdangerous`等 7 個依賴套件,實際上總共安裝了 8 個套件!
|
||
|
||
可以說,pip 在「安裝」套件時的相依性管理還是可以的,這並不難,因為套件的依賴要求都寫在安裝檔裡了,根本不需要「管理」。
|
||
|
||
---
|
||
|
||
附帶一提,這 8 個套件包括`flask`,除了`importlib-metadata`和`zipp`外,其餘 6 個實際上都是由 [Flask 團隊自行開發](https://palletsprojects.com/p/)。
|
||
|
||
但並非只有 Flask 框架會使用(依賴)這些套件。
|
||
|
||
比如其中的 [Click](https://palletsprojects.com/p/click/) 就是一個被廣泛使用的命令列製作工具。套件官網是這麼介紹的:
|
||
|
||
> Click is a Python package for **creating beautiful command line interfaces** in a composable way with as little code as necessary.
|
||
|
||
別的套件也可能依賴`click`來提供命令列的功能,換句話說,主套件的依賴套件也可能被其他第三方套件所依賴、使用。**這就產生了「衝突」的可能。**
|
||
|
||
---
|
||
|
||
好,一切都很美好,就這樣一年過去,團隊決定改用火紅的 FastAPI 取代 Flask 來實作專案的 API,作為 API 的主要開發人員,你對新技術充滿了期待(或排斥),興高采列地安裝了 FastAPI,更新了所有程式碼,最後要移除 Flask,這時問題就來了。
|
||
|
||
安裝 Flask 的時候,我們只需要`pip install flask`,pip 就會幫你一併安裝所有依賴套件。現在要移除它,也只要`pip uninstall flask`就可以了嗎?
|
||
|
||
> **很遺憾,答案是否定的**。
|
||
|
||
### pip 的致命缺陷:缺乏移除套件時的依賴解析(相依性管理)
|
||
|
||
僅執行`pip uninstall flask`的話,pip 就**真的只會**幫你移除`flask`這個套件**本身**而已。那剩下的、再也用不到的套件怎麼辦?你只能一個一個手動移除!
|
||
|
||
但你千萬不要真的嘗試手動移除依賴套件!——因為你無法確定這些依賴套件**是否同時被別的套件所依賴**。
|
||
|
||
### pip 手動移除依賴套件的潛在風險:以 Flask + Black 為例
|
||
|
||
繼續以 Flask 為例,還記得其中一個依賴套件是`click`,如前所述,它是一個協助製作命令列界面的工具。
|
||
|
||
假設專案中已同時安裝了 [Black](https://github.com/psf/black) 這個 Python code formatter 來統一程式碼排版(沒錯!我現在個人開發也都改用 Black 取代 [yapf](https://github.com/google/yapf) 了),Black 是一個可以透過 CLI 指令執行的格式化工具,剛好,它也是使用`click`來實作命令列功能。
|
||
|
||
> **Black formatter** 相關文章:
|
||
>
|
||
> - [試用從 VS Code Python extension 拆分的 Black、isort 套件](https://blog.kyomind.tw/black-and-isort/)
|
||
> - [pyproject.toml 介紹 + VS Code 整合 Black、yapf、isort 教學](https://blog.kyomind.tw/pyproject-toml/)
|
||
|
||
我們可以藉由 Poetry 指令(後續介紹)來查看,目前這唯二安裝套件的**依賴關係樹**:
|
||
|
||
```
|
||
black 22.3.0 The uncompromising code formatter.
|
||
├── click >=8.0.0
|
||
│ └── colorama *
|
||
├── mypy-extensions >=0.4.3
|
||
├── pathspec >=0.9.0
|
||
├── platformdirs >=2
|
||
├── tomli >=1.1.0
|
||
└── typing-extensions >=3.10.0.0
|
||
flask 2.1.2 A simple framework for building complex web applications.
|
||
├── click >=8.0
|
||
│ └── colorama *
|
||
├── importlib-metadata >=3.6.0
|
||
│ └── zipp >=0.5
|
||
├── itsdangerous >=2.0
|
||
├── jinja2 >=3.0
|
||
│ └── markupsafe >=2.0
|
||
└── werkzeug >=2.0
|
||
```
|
||
|
||
可以明顯看出,**兩者都依賴了`click`套件**。可想而知,移除 Flask 時,如果你同時把`click`也**跟著一併移除**,會發生什麼樣的悲劇——**你的 Black 壞了**——因為它缺少了必要的依賴套件`click`。
|
||
|
||
簡言之,直接 pip 手動移除依賴套件存在下列兩大疑慮,不建議輕易嘗試:
|
||
|
||
#### 一、無法確定想移除的套件還有多少依賴套件
|
||
|
||
正常而言,你不會去注意安裝時總共一併安裝了多少依賴套件。雖然有`pip show`這類的指令可以大概知曉套件的依賴,但這指令只會顯示「**直接**依賴套件」而不會顯示「依賴套件的依賴」,所以列出來的結果未必準確:
|
||
|
||
```bash
|
||
❯ pip show flask
|
||
Name: Flask
|
||
Version: 2.1.1
|
||
Summary: A simple framework for building complex web applications.
|
||
Home-page: https://palletsprojects.com/p/flask
|
||
Author: Armin Ronacher
|
||
Author-email: armin.ronacher@active-4.com
|
||
License: BSD-3-Clause
|
||
Location: /Users/kyo/.pyenv/versions/3.8.12/envs/test/lib/python3.8/site-packages
|
||
Requires: importlib-metadata, Werkzeug, click, Jinja2, itsdangerous
|
||
Required-by:****
|
||
```
|
||
|
||
可以看到,`Requires:`只顯示了 5 個依賴套件,因為剩下的 2 個(`zipp`、`markupsafe`)是「**依賴的依賴**」,在更下層,並未顯示。
|
||
|
||
#### 二、即使確定所有依賴套件,也無法確定這些套件是否還被其他套件所依賴
|
||
|
||
好繞口啊!上述的`click`例子就是解釋這個困境。
|
||
|
||
### 小結:pip 只適合小型專案或「只新增不移除」套件的專案
|
||
|
||
以前我的個人或工作上的專案往往規模不大,pip 就真的只負責新增,鮮少需要考慮移除套件的情況,所以缺少移除套件時的依賴解析,似乎也沒什麼大問題。
|
||
|
||
但稍具模規的專案往往就需要考慮套件的退場,以維持開發及部署環境的簡潔,尤其在使用容器化部署時,**過多不必要的套件會徒增 image 肥大,產生額外的成本與資源浪費不說,同時也提升了「套件之間發生衝突」的可能。**
|
||
|
||
然而透過上述的例子可知,僅靠 pip 想要**乾淨移除**過時的套件,且不影響既有的套件,簡直是**不可能的任務!**所以我們需要擁有「**完整套件相依性管理功能**」的套件管理器。
|
||
|
||
---
|
||
|
||
## pip 替代方案選擇——Pipenv vs Poetry
|
||
|
||
關於 pip 的前世今生、它的歷史包袱,以及為何它難以演化成理想的、可以完美管理套件相依性的模樣,可以參考〈[告別 Anaconda:在 macOS 上使用 pyenv 建立 Python 開發環境](https://blog.kyomind.tw/pyenv-setup/)〉中推薦過的單集 Podcast:
|
||
|
||
- [《捕蛇者說》Ep 15. 和 PyPA 的成員聊聊 Python 開發工作流](https://pythonhunter.org/episodes/ep15)
|
||
|
||
從 Podcast 官方頁面中「時間節點」目錄中可知,該集對 Python 的虛擬環境與套件管理機制及相關工具,有著非常廣泛的討論,十分精彩,強力推薦!(為了寫這篇又聽了第 3 次)
|
||
|
||

|
||
|
||
### Pipenv vs Poetry
|
||
|
||
因為 pip 存在這樣的致命弱點,所以很早就有相關的方案提出想要解決它,最知名的莫過於 [Pipenv](https://pipenv.pypa.io/en/latest/)!
|
||
|
||
而講到需要有充分「套件相依性管理」功能的套件管理器,你基本上也只能從 Pipenv 和 Poetry 兩者之中二擇一了。
|
||
|
||
如果是在兩年前,這道選擇題恐怕不容易回答,且 Pipenv 可能會有較大的機率勝出,**但兩年後的今天,我會建議你毫不猶豫地選擇 Poetry。**
|
||
|
||
## 選擇 Poetry 的兩個理由
|
||
|
||
怎麼說呢?以下是我的理由。
|
||
|
||
### 理由一:Pipenv 的不足
|
||
|
||
當你搜尋「python poetry」關鍵字的時候,那些教你怎麼使用 Poetry 的文章,往往也會一併提及**為何不選擇 Pipenv。**
|
||
|
||
以下兩篇有著較為完整的說明,容我直接引用。
|
||
|
||
〈[Python - 取代 Pipenv 的新套件管理器 Poetry](https://note.koko.guru/posts/using-poetry-manage-python-package-environments)〉:
|
||
|
||
> Pipenv 雖然強大,卻也暴露出了一些問題如 Lock 過慢、Windows 支援性差、對 PyPI 套件打包的友善度差…等更多其他問題,甚至有越來越多人表明 [不要使用 Pipenv](http://greyli.com/do-not-use-pipenv/) 或 [pipenv 的凋零與替代方案 poetry](https://blog.gslin.org/archives/2019/12/21/9347/pipenv-%E7%9A%84%E5%87%8B%E9%9B%B6%E8%88%87%E6%9B%BF%E4%BB%A3%E6%96%B9%E6%A1%88-poetry/) 等。
|
||
|
||
> 同時 Pipenv 的社群維護狀況也越來越差,有許多的 PR 都沒有被 Release,導致許多貢獻者抱怨,甚至有人發出了該篇 [If this project is dead, just tell us](https://github.com/pypa/pipenv/issues/4058) issue 想知道是否專案已經不在維護。
|
||
|
||
〈[相比 Pipenv,Poetry 是一個更好的選擇](https://greyli.com/poetry-a-better-choice-than-pipenv/)〉(本文作者[李輝](https://greyli.com/about/)為 Flask 團隊成員):
|
||
|
||
> Pipenv 描繪了一個美夢,讓我們以為 Python 也有了其他語言那樣完善的包管理器,不過這一切卻在後來者 Poetry 這裡得到了更好的實現。
|
||
|
||
> 這幾年 Pipenv 收獲了很多用戶,但是也暴露了很多問題。雖然 Lock 太慢、Windows 支持不好和 bug 太多的問題都已經改進了很多,但對我來說,仍然不能接受隨時更新鎖定依賴的設定,在上一篇文章《[不要用 Pipenv](http://greyli.com/do-not-use-pipenv/)》裡也吐槽了很多相關的問題。
|
||
|
||
兩篇的引述內容總結就是一句話:不要用 Pipenv。
|
||
|
||
目前 Pipenv 已經由 [PyPA](https://github.com/pypa)(同時也維護 pip 及 virtualenv)接手,上述「**擺爛**」的情況相信已有所改善,不過我似乎還沒看到有什麼文章大力鼓吹或宣告 Pipenv 已經「great again」,所以個人對它的未來發展還是持保留態度。
|
||
|
||
### 理由二:pyproject.toml
|
||
|
||
pyproject.toml 是 [PEP 518](https://peps.python.org/pep-0518/) 所提出的新標準:
|
||
|
||
> The build system dependencies will be stored in a file named `pyproject.toml` that is written in the TOML format.
|
||
|
||
原意是作為套件打包設定的標準格式,後來又有了 [PEP 621](https://peps.python.org/pep-0621/),將其**擴充定性**為 **Python 生態系工具的共同設定檔標準**,現在已經被愈來愈多套件所支援,詳細可參考[這個清單](https://github.com/carlosperate/awesome-pyproject)及頁面中的說明:
|
||
|
||
> `pyproject.toml` is a new configuration file defined in [PEP 518](https://www.python.org/dev/peps/pep-0518/) and expanded in [PEP 621](https://www.python.org/dev/peps/pep-0621/). It is design to store build system requirements, **but it can also store any tool configuration for your Python project**, possibly replacing the need for `setup.cfg` or other tool-specific files.
|
||
|
||
作為規範控,我很願意追隨這個標準。
|
||
|
||
並且,Poetry 使用`pyproject.toml`,可遠遠不止是設定檔的程度,更是**不可或缺**的一部分。相當於 Pipenv 的`Pipfile`或 npm 的`package.json`。
|
||
|
||
換句話說,少了`pyproject.toml`,Poetry 便無法運作。
|
||
|
||
> 更多關於 **pyproject.toml** 的介紹與實踐,可參考:[pyproject.toml 介紹 + VS Code 整合 Black、yapf、isort 教學](https://blog.kyomind.tw/pyproject-toml/)
|
||
|
||
---
|
||
|
||
好,漫長的前言到此結束,讓我們進入正題,開始上手學習 Poetry。
|
||
|
||
## 從零開始使用 Poetry
|
||
|
||
本文所有的參考資料會放在文末的「參考」一欄中,不過在此還是要特別提及主要的參考對象,總共有二:
|
||
|
||
- [Poetry 官方文件](https://python-poetry.org/docs/)
|
||
- [Dependency Management With Python Poetry](https://realpython.com/dependency-management-python-poetry/)
|
||
|
||
如果在本文找不到你需要的內容,以上二處可能會有,所以主動列出。
|
||
|
||
另外本文主要以 macOS 和 Linux(Ubuntu)環境來進行安裝及教學,Windows 用戶如果有無法順利安裝的情況,建議參考官方文件內容修正。不過,即使有問題,應該也是集中在安裝與設定階段,本文其餘部分仍可適用。
|
||
|
||
## 安裝 Poetry
|
||
|
||
Poetry 和 pip、git、pyenv 等工具一樣,都是典型的**命令列工具**,需要先安裝才能下達指令——`poetry`。
|
||
|
||
### 安裝方式選擇
|
||
|
||
Poetry 主要提供了[兩種安裝方式](https://python-poetry.org/docs/#installation):
|
||
|
||
1. **全域安裝**至使用者的家目錄。
|
||
2. **pip 安裝**至專案使用的 Python(虛擬)環境,即`pip install poetry`。
|
||
|
||
**個人推薦使用全域安裝**,官方文件也表示[不推薦使用 pip 安裝](https://python-poetry.org/docs/#alternative-installation-methods-not-recommended)。
|
||
|
||
因為 pip 安裝是直接安裝到「**專案所屬的 Python 虛擬環境**」裡,而 Poetry 所依賴的套件非常多,**總計超過 30 個,會嚴重影響專案虛擬環境的整潔度**。文件中也警告這些依賴套件可能和專案本身的套件**發生衝突**:
|
||
|
||
> Be aware that it will also install Poetry’s dependencies which **might cause conflicts with other packages.**
|
||
|
||
### 全域安裝 Poetry 至家目錄
|
||
|
||
所以我們就使用全域安裝吧!
|
||
|
||
#### macOS / Linux / WSL(Windows Subsystem for Linux)
|
||
|
||
```
|
||
curl -sSL https://install.python-poetry.org | python3 -
|
||
```
|
||
|
||
或
|
||
|
||
```
|
||
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
|
||
```
|
||
|
||
#### Windows
|
||
|
||
```
|
||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
||
```
|
||
|
||
Poetry 實際安裝路徑如下:
|
||
|
||
> The installer installs the `poetry` tool to Poetry’s `bin` directory. This location depends on your system:
|
||
|
||
- `$HOME/.local/bin` for Unix
|
||
- `%APPDATA%\Python\Scripts` on Windows
|
||
|
||
以 macOS 為例,如果要下`poetry`指令,就需要打完整路徑`$HOME/.local/bin/poetry`,顯然不太方便,所以我們需要設定 PATH。
|
||
|
||
### 設定 PATH
|
||
|
||
新增`poetry`指令執行檔所在的路徑至 PATH。
|
||
|
||
在`.zshrc`或`.bashrc`或`.bash_profile`新增:
|
||
|
||
```
|
||
export PATH=$PATH:$HOME/.local/bin
|
||
```
|
||
|
||
存檔後重啟 shell 即可使用。直接在命令列打上`poetry`指令測試:
|
||
|
||
```
|
||
❯ poetry
|
||
Poetry version 1.1.13
|
||
|
||
USAGE
|
||
poetry [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]
|
||
|
||
...
|
||
```
|
||
|
||
### 設定 alias
|
||
|
||
比起`pip`,`poetry`這個指令實在太冗長了!我們還是給它一個 alias 吧!
|
||
|
||
基於它是我極為常用的指令,我願意賦與它**「單字母」的 alias 特權**,我使用`p`:
|
||
|
||
```
|
||
alias p='poetry'
|
||
```
|
||
|
||
測試結果:
|
||
|
||
```
|
||
❯ p
|
||
Poetry version 1.1.13
|
||
|
||
USAGE
|
||
poetry [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]
|
||
```
|
||
|
||
alias 是方便自己使用,但本文基於表達清晰考量,下面的解說除了圖片外,原則上並不會使用 alias 表示。
|
||
|
||
---
|
||
|
||
## 初始化 Poetry 專案
|
||
|
||
為了方便解說,我們先建立一個全新的專案,名為`poetry-demo`。
|
||
|
||
指令都很簡單,但還是建議可以一步一步跟著操作。
|
||
|
||
就像 git 專案需要初始化,Poetry 也需要,因為每一個使用了 Poetry 的專案中一定要有一個`pyproject.toml`作為它的**設定檔**。否則直接使用`poetry`相關指令就會出現下列錯誤訊息:
|
||
|
||
> Poetry could not find a pyproject.toml file in {cwd} or its parents
|
||
|
||
所以一定先初始化,使用`poetry init`:
|
||
|
||
```
|
||
mkdir poetry-demo
|
||
cd poetry-demo
|
||
poetry init
|
||
```
|
||
|
||
此時會跳出一連串的互動對話,協助你建立專案的資料,大部分可以直接`enter`跳過:
|
||
|
||
```
|
||
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]
|
||
```
|
||
|
||
直到出現「`Would you like to define your main dependencies interactively? (yes/no) [yes]`」,我們先選擇「**no**」後,會讓你確認本次產生的`toml`檔內容:
|
||
|
||
```
|
||
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>"]
|
||
|
||
[tool.poetry.dependencies]
|
||
python = "^3.8"
|
||
|
||
[tool.poetry.dev-dependencies]
|
||
|
||
[build-system]
|
||
requires = ["poetry-core>=1.0.0"]
|
||
build-backend = "poetry.core.masonry.api"
|
||
```
|
||
|
||
並詢問你「`Do you confirm generation? (yes/no) [yes]`」,按`enter`使用預設選項「yes」或直接回答「yes」,則`pyproject.toml`建立完成。
|
||
|
||
此時專案目錄結構如下:
|
||
|
||
```
|
||
poetry-demo
|
||
└── pyproject.toml
|
||
|
||
0 directories, 1 file
|
||
```
|
||
|
||
## 管理 Poetry 虛擬環境
|
||
|
||
我覺得學習 Poetry 的**第一道關卡**,就是它對於虛擬環境的管理。
|
||
|
||
### 「強制」虛擬環境
|
||
|
||
Poetry 預設上(可透過`poetry config`修改)會強制套件都要安裝在虛擬環境中,以免污染全域,所以它整合了`virtualenv`。
|
||
|
||
所以在執行`poetry add、install`等指令時,Poetry 都會自動檢查**當下是否正在使用虛擬環境:**
|
||
|
||
- 如果**是**,則會直接安裝套件至**當前**的虛擬環境。
|
||
- 如果**否**,則會自動幫你建立一個**新的**虛擬環境,再進行套件安裝。
|
||
|
||
### 容易混淆的虛擬環境
|
||
|
||
Poetry 主動納入虛擬環境管理算是立意良善,相當於把`pip`+`venv`兩者的功能直接整合在一起,**但也帶來一定的複雜度**,尤其在你已經自行使用了`venv`、`virtualenv`或 `pyenv-virtualenv`或`conda`等工具來管理虛擬環境的情況下!
|
||
|
||
**沒錯,Python 的虛擬環境管理就是這麼麻煩!**
|
||
|
||
個人建議,對新手而言,於 Poetry 的專案中,**一律使用 Poetry** 來管理虛擬環境即可。我目前也是這樣,省得麻煩。
|
||
|
||
### 以指令建立虛擬環境
|
||
|
||
使用指令`poetry env use python`:
|
||
|
||
```
|
||
❯ poetry env use python
|
||
Creating virtualenv poetry-demo-IEWSZKSE-py3.8 in /Users/kyo/Library/Caches/pypoetry/virtualenvs
|
||
Using virtualenv: /Users/kyo/Library/Caches/pypoetry/virtualenvs/poetry-demo-IEWSZKSE-py3.8
|
||
```
|
||
|
||
可以看出 Poetry 為我們建立了名為`poetry-demo-IEWSZKSE-py3.8`的虛擬環境。
|
||
|
||
### 重點說明
|
||
|
||
- `poetry env use python`建立虛擬環境所使用的 Python 版本,取決於`python`指令在你的 PATH 是連結到哪個版本。同理,你也可以將指令明示為`use python3`或`use python3.8`,只要這些指令確實存在 PATH 中。
|
||
- 預設上,Poetry 會統一將虛擬環境建立在「**特定目錄**」裡,比如本例中存放的路徑是`/Users/kyo/Library/Caches/pypoetry/virtualenvs`。
|
||
- 虛擬環境的**命名模式為`專案名稱-亂數-Python版本`。**
|
||
|
||
老實說我個人不是很喜歡這樣的做法,因為這意味著單一專案允許建立複數個虛擬環境(比如 Python 3.7、3.8、3.9 可以各來一個),**彈性之餘也增加了混亂的可能**,而且這命名模式我也不太欣賞,顯得過於僵化且冗長。
|
||
|
||
既然 Python 的虛擬環境理論上都是**高度綁定專案本身**的,我更偏好`venv`式的做法,也就是**把虛擬環境放到專案目錄內**,而非統一放在獨立的目錄,讓虛擬環境與專案呈現**直觀的一對一關係**。
|
||
|
||
所幸,Poetry 具備這樣的選項。
|
||
|
||
---
|
||
|
||
### 修改`config`,建立專案內的`.venv`虛擬環境
|
||
|
||
我們先使用`poetry config`指令來查看 Poetry 目前幾個主要的設定,需要`--list`這個參數:
|
||
|
||
```
|
||
❯ poetry config --list
|
||
cache-dir = "/Users/kyo/Library/Caches/pypoetry"
|
||
experimental.new-installer = true
|
||
installer.parallel = true
|
||
virtualenvs.create = true
|
||
virtualenvs.in-project = false
|
||
virtualenvs.path = "{cache-dir}/virtualenvs"
|
||
```
|
||
|
||
其中`virtualenvs.create = true`若改成`false`,則可以停止 Poetry 在「偵測不到虛擬環境時會自行建立」的行為模式,但建議還是不要更動。
|
||
|
||
而`virtualenvs.in-project = false`就是我們要修改的目標,使用指令:
|
||
|
||
```
|
||
poetry config virtualenvs.in-project true
|
||
```
|
||
|
||
好,我們先把之前建立的虛擬環境刪除:
|
||
|
||
```
|
||
❯ poetry env remove python
|
||
Deleted virtualenv: /Users/kyo/Library/Caches/pypoetry/virtualenvs/poetry-demo-IEWSZKSE-py3.8
|
||
```
|
||
|
||
重新建立,看看行為有何差異:
|
||
|
||
```
|
||
❯ poetry env use python
|
||
Creating virtualenv poetry-demo in /Users/kyo/Documents/code/poetry-demo/.venv
|
||
Using virtualenv: /Users/kyo/Documents/code/poetry-demo/.venv
|
||
```
|
||
|
||
可以看出:
|
||
|
||
- 虛擬環境的路徑改為「**專案的根目錄**」。
|
||
- 名稱固定為`.venv`。
|
||
|
||
我覺得這樣的設定更加簡潔。
|
||
|
||
### 啟動與退出虛擬環境
|
||
|
||
啟動虛擬環境,需移至專案目錄底下,使用指令`poetry shell`:
|
||
|
||
1
|
||
2
|
||
3
|
||
```
|
||
❯ poetry shell
|
||
Spawning shell within /Users/kyo/Documents/code/poetry-demo/.venv
|
||
❯ . /Users/kyo/Documents/code/poetry-demo/.venv/bin/activate
|
||
```
|
||
|
||
`poetry shell`指令會偵測當前目錄或所屬上層目錄是否存在`pyproject.toml`來確定所要啟動的虛擬環境,所以如果不移至專案目錄,則會出現下列錯誤:
|
||
|
||
```
|
||
❯ poetry shell
|
||
|
||
RuntimeError
|
||
|
||
Poetry could not find a pyproject.toml file in /Users/kyo/Documents/code or its parents
|
||
|
||
at ~/Library/Application Support/pypoetry/venv/lib/python3.8/site-packages/poetry/core/factory.py:369 in locate
|
||
365│ if poetry_file.exists():
|
||
366│ return poetry_file
|
||
367│
|
||
368│ else:
|
||
→ 369│ raise RuntimeError(
|
||
370│ "Poetry could not find a pyproject.toml file in {} or its parents".format(
|
||
371│ cwd
|
||
372│ )
|
||
373│ )
|
||
```
|
||
|
||
可以看到,Poetry 的錯誤訊息非常清楚,讓你很容易知曉修正的方向,這是作為一個優秀命令列工具的必要條件。
|
||
|
||
退出就簡單多了,只需要`exit`即可。
|
||
|
||
---
|
||
|
||
## Poetry 常用指令
|
||
|
||
Poetry 是一個獨立的命令列工具,就像 pyenv,它有自己的指令,需要花費額外的心力學習,且較 pip 更加複雜,這可能是使用 Poetry 的**第二道關卡**。好在常用的指令,其實也不超過 10 個,下面就來一一介紹。
|
||
|
||
在此我們繼續使用前面提過的 Flask 和 Black 套件,來示範並說明 Poetry 的優勢以及它和 pip 的不同之處。
|
||
|
||
### Poetry 新增套件
|
||
|
||
使用指令:
|
||
|
||
```
|
||
poetry add
|
||
```
|
||
|
||
相當於`pip install`,我們來試著安裝 Flask 看看會有什麼變化:
|
||
|
||

|
||
|
||
圖中可以看出 Poetry 漂亮的命令列資訊呈現,會清楚告知總共新增了幾個套件。
|
||
|
||
此時專案中的`pyproject.toml`也會發生變化:
|
||
|
||
```
|
||
...
|
||
[tool.poetry.dependencies]
|
||
python = "^3.8"
|
||
Flask = "^2.1.1" # 新增部分
|
||
|
||
[tool.poetry.dev-dependencies]
|
||
|
||
[build-system]
|
||
...
|
||
```
|
||
|
||
這裡要說明,安裝 Flask,則`pyproject.toml`就只會新增記載`Flask = "^2.1.1"`這個 **top-level** 的 package 項目,其餘的依賴套件**不會**直接記錄在`toml`檔中。
|
||
|
||
我覺得這是一大優點,方便區分哪些是你**主動安裝**的主要套件,而哪些又是基於套件的依賴關係而一併安裝的依賴套件。
|
||
|
||
### poetry.lock 與更新順序
|
||
|
||
除了更新`pyproject.toml`,此時專案中還會新增一個檔案,名為`poetry.lock`,它實際上就相當於 pip 的`requirements.txt`,詳細記載了所有安裝的套件與版本。
|
||
|
||
當你使用`poetry add`指令時,Poetry 會**自動依序**幫你做完這三件事:
|
||
|
||
1. 更新`pyproject.toml`。
|
||
2. 依照`pyproject.toml`的內容,更新`poetry.lock`。
|
||
3. 依照`poetry.lock`的內容,更新虛擬環境。
|
||
|
||
由此可見,`poetry.lock`的內容是取決於`pyproject.toml`,但兩者並不會自己連動,一定要基於特定指令才會進行同步與更新,`poetry add`就是一個典型案例。
|
||
|
||
此時專案目錄結構如下:
|
||
|
||
```
|
||
poetry-demo
|
||
├── poetry.lock
|
||
└── pyproject.toml
|
||
|
||
0 directories, 2 files
|
||
```
|
||
|
||
### 更新 poetry.lock
|
||
|
||
當你自行修改了`pyproject.toml`內容,比如變更特定套件的版本(這是有可能的,尤其在手動處理版本衝突的時候),此時`poetry.lock`的內容與`pyproject.toml`出現了「**脫鉤**」,必須讓它依照新的`pyproject.toml`內容更新、同步,使用指令:
|
||
|
||
```
|
||
poetry lock
|
||
```
|
||
|
||
如此一來,才能確保手動修改的內容,也更新到`poetry.lock`中,畢竟虛擬環境如果要重新建立,是基於`poetry.lock`的內容來安裝套件,而非`pyproject.toml`。
|
||
|
||
還是那句話:`poetry.lock`相當於 Poetry 的`requirements.txt`。
|
||
|
||
---
|
||
|
||
### 列出全部套件清單
|
||
|
||
類似`pip list`,這裡要使用`poetry show`:
|
||
|
||
```
|
||
❯ poetry show
|
||
black 22.3.0 The uncompromising code formatter.
|
||
click 8.1.3 Composable command line interface toolkit
|
||
flask 2.1.2 A simple framework for building complex web applications.
|
||
importlib-metadata 4.11.4 Read metadata from Python packages
|
||
itsdangerous 2.1.2 Safely pass data to untrusted environments and back.
|
||
jinja2 3.1.2 A very fast and expressive template engine.
|
||
markupsafe 2.1.1 Safely add untrusted strings to HTML/XML markup.
|
||
mypy-extensions 0.4.3 Experimental type system extensions for programs checked...
|
||
pathspec 0.9.0 Utility library for gitignore style pattern matching of ...
|
||
platformdirs 2.5.2 A small Python module for determining appropriate platfo...
|
||
...
|
||
```
|
||
|
||
特別提醒的是,這裡的清單內容**並不是來自於虛擬環境**,這點和 pip 不同,而是來自於`poetry.lock`的內容。
|
||
|
||
你可能會想,來自於`poetry.lock`或虛擬環境,有差嗎?兩者不是應該要一致?
|
||
|
||
沒錯,理論上是,但也有不一致的時候,比如你使用了`pip install`指令安裝套件,就不會記載在`poetry.lock`中,那`poetry show`自然也不會顯示。
|
||
|
||
### 「樹狀」顯示套件依賴層級
|
||
|
||
Poetry 最為人津津樂道的就是它的樹狀顯示——`poetry show --tree`。
|
||
|
||
```
|
||
❯ poetry show --tree
|
||
flask 2.1.1 A simple framework for building complex web applications.
|
||
├── click >=8.0
|
||
│ └── colorama *
|
||
├── importlib-metadata >=3.6.0
|
||
│ └── zipp >=0.5
|
||
├── itsdangerous >=2.0
|
||
├── jinja2 >=3.0
|
||
│ └── markupsafe >=2.0
|
||
└── werkzeug >=2.0
|
||
black 22.3.0 The uncompromising code formatter.
|
||
├── click >=8.0.0
|
||
│ └── colorama *
|
||
├── mypy-extensions >=0.4.3
|
||
├── pathspec >=0.9.0
|
||
├── platformdirs >=2
|
||
├── tomli >=1.1.0
|
||
└── typing-extensions >=3.10.0.0
|
||
```
|
||
|
||
讓主要套件與其依賴套件的**關係與層次,一目了然**。
|
||
|
||
而且很貼心的是,它也可以**只顯示「指定套件」**的依賴層級,以`celery`為例:
|
||
|
||
```
|
||
❯ poetry show celery --tree
|
||
celery 4.4.0 Distributed Task Queue.
|
||
├── billiard >=3.6.1,<4.0
|
||
├── kombu >=4.6.7,<4.7
|
||
│ ├── amqp >=2.6.0,<2.7
|
||
│ │ └── vine >=1.1.3,<5.0.0a1
|
||
│ └── importlib-metadata >=0.18
|
||
│ ├── typing-extensions >=3.6.4
|
||
│ └── zipp >=0.5
|
||
├── pytz >0.0-dev
|
||
└── vine 1.3.0
|
||
```
|
||
|
||
### 安裝套件至 dev-dependencies
|
||
|
||
有些套件,比如`pytest`、`flake8`等等,**只會在開發環境中使用**,產品的**部署環境**並不需要安裝。
|
||
|
||
Poetry 允許你區分這兩者,將上述的套件安裝至`dev-dependencies`區塊,方便讓你**輕鬆建立一份不包含 dev 套件的安裝清單**。
|
||
|
||
在此以 Black 為例,安裝方式如下:
|
||
|
||
```
|
||
poetry add black -D
|
||
```
|
||
|
||
或
|
||
|
||
```
|
||
poetry add black --dev
|
||
```
|
||
|
||
結果的區別顯示在`pyproject.toml`裡:
|
||
|
||
```
|
||
...
|
||
[tool.poetry.dependencies]
|
||
python = "^3.8"
|
||
Flask = "^2.1.1"
|
||
|
||
[tool.poetry.dev-dependencies]
|
||
black = "^22.3.0"
|
||
...
|
||
```
|
||
|
||
可以看到`black`被列在不同區塊:`tool.poetry.dev-dependencies`。
|
||
|
||
#### 建議:明確區分 dev-dependencies
|
||
|
||
明確區分出**開發環境專用**的套件,並從部署環境中分離,我認為很有必要。
|
||
|
||
因為這些套件常常屬於「檢測型」工具,相關的**依賴套件**著實都不少!比如`flake8`,它依賴了`pycodestyle`、`pyflakes`、`mccabe`等等,這些套件都是開發環境才需要,而且它們的版本也不一定要與產品的版本一致。
|
||
|
||
還有`black`、`pre-commit`等,依賴的套件數量也都很可觀,如果不加以區分一律安裝到`dependencies`區塊,部署環境就會顯得過於**臃腫**。
|
||
|
||
常見的`dev-dependencies`區塊項目例示如下:
|
||
|
||
```toml
|
||
[tool.poetry.dev-dependencies]
|
||
flake8 = "4.0.1"
|
||
yapf = "0.32.0"
|
||
pytest = "7.1.2"
|
||
pytest-django = "4.5.2"
|
||
pytest-cov = "3.0.0"
|
||
pytest-env = "0.6.2"
|
||
pytest-sugar = "0.9.4"
|
||
pre-commit = "2.20.0"
|
||
```
|
||
|
||
---
|
||
|
||
### Poetry 移除套件
|
||
|
||
使用`poetry remove`指令。和`poetry add`一樣,可以加上`-D`參數來移除置於開發區的套件。
|
||
|
||
而移除套件時的「**依賴解析(相依性管理)**」能力,正是 Poetry 遠優於 pip 的主要環節,因為 pip 沒有嘛!也是我提議改用 Poetry 的關鍵理由——**為了順利移除套件**。
|
||
|
||
前面已經提過,pip 的`pip uninstall`只會移除你所指定的套件,而不會連同依賴套件一起移除。
|
||
|
||
這是基於安裝考量,因為 pip 沒有「依賴解析」功能。如果貿然移除所有「安裝時一併安裝」的依賴套件,可能會造成巨大災難,讓別的套件失去效用。
|
||
|
||
前面也舉了 Flask 和 Black 都共同依賴`click`這個套件的例子,在手動移除套件的情況下,你可能未曾注意 Black 也依賴了`click`,結果為了「徹底移除」Flask 的所有相關套件,不小心把`click`也移除掉了。
|
||
|
||
當然,我知道,絕大部分的真實情況是——你根本不會去移除一段時間前安裝但已不再使用的套件。
|
||
|
||
---
|
||
|
||
好,解釋了很多,接下來就是 Poetry 的表演了,它會幫你處理這些棘手的「套件相依性」難題,讓你輕鬆移除 Flask 而不影響 Black:
|
||
|
||

|
||
|
||
可以對比上面安裝 Flask 時的截圖,那時總共安裝了 8 個套件,但現在移除的卻只有 7 個——沒錯,因為有依賴解析,**Poetry 知道 Black 還需要**`click`!所以不能移除:
|
||
|
||
```
|
||
❯ poetry show --tree
|
||
black 22.3.0 The uncompromising code formatter.
|
||
├── click >=8.0.0
|
||
│ └── colorama *
|
||
├── mypy-extensions >=0.4.3
|
||
├── pathspec >=0.9.0
|
||
├── platformdirs >=2
|
||
├── tomli >=1.1.0
|
||
└── typing-extensions >=3.10.0.0
|
||
```
|
||
|
||
一個套件直到環境中的**其餘套件都不再依賴它**,Poetry 才會安心讓它被移除。
|
||
|
||
### 輸出 Poetry 虛擬環境的 requirements.txt
|
||
|
||
理論上,全面改用 Poetry 後,專案中是不需要存在`requirements.txt`,因為它的角色已經完全被`poetry.lock`所取代。
|
||
|
||
但事實是,你可能還需要它,甚至還需要它隨著`poetry.lock`的內容更新!至少對我而言就是如此,我在 Docker 部署環境中並不使用 Poetry,所以我需要一份完全等價於`poetry.lock`的`requirements.txt`,用於 Docker 部署。
|
||
|
||
你可能想說,那我就在 Poetry 的虛擬環境下,使用以往熟悉的指令`pip freeze > requirements.txt`來產生一份就可以了吧?我本來也是這麼想,但實際的產出卻是如此:(提醒:目前 poetry-demo 專案中**僅剩下 Black 和它的依賴套件**)
|
||
|
||
```
|
||
black @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/11/4c/fc/cd6d885e9f5be135b161e365b11312cff5920d7574c8446833d7a9b1a3/black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl
|
||
click @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/f0/23/09/b13d61d1fa8b3cd7c26f67505638d55002e7105849de4c4432c28e1c0d/click-8.1.2-py3-none-any.whl
|
||
mypy-extensions @ file:///Users/kyo/Library/Caches/pypoetry/artifacts/b6/a0/b0/a5dc9acd6fd12aba308634f21bb7cf0571448f20848797d7ecb327aa12/mypy_extensions-0.4.3-py2.py3-none-any.whl
|
||
...
|
||
```
|
||
|
||
這呈現好像不是我們以前熟悉的那樣:
|
||
|
||
```
|
||
black==22.3.0
|
||
click==8.1.2
|
||
mypy_extensions==0.4.3
|
||
...
|
||
```
|
||
|
||
沒錯,只要是使用`poetry add`安裝的套件,在`pip freeze`就會變成這樣。此時想輸出類似`requirements.txt`的格式,需要使用`poetry export`。
|
||
|
||
預設的輸出結果會有 hash 值,很干擾閱讀。不想納入 hash 則要**加上參數**去除。**以下就是我固定用來輸出`requirements.txt`的指令與參數:**
|
||
|
||
```
|
||
poetry export -f requirements.txt -o requirements.txt --without-hashes
|
||
```
|
||
|
||
`2022/08/24`補充:網友提醒,**hash 有其價值,並建議保留**,詳見[留言區](https://github.com/kyomind/blog-reply/issues/5#issuecomment-1195904820)。
|
||
|
||
我們再看一下輸出結果,雖然不盡相同,但也相去不遠了……嗎?等等,怎麼是空白?
|
||
|
||
---
|
||
|
||
因為`poetry export`預設只會輸出`toml`中的`[tool.poetry.dependencies]`區塊的套件!還記得上面我們把 Black 安裝到`[tool.poetry.dev-dependencies]`了嗎?
|
||
|
||
顯然 Poetry 認為你的 export 需求基本上就為了部署,並不需要開發區的套件。這倒是沒錯,不過基於演示需求,我們必須輸出`[tool.poetry.dev-dependencies]`的套件,才能看到 Black。
|
||
|
||
加上`--dev`參數即可:
|
||
|
||
```
|
||
poetry export -f requirements.txt -o requirements.txt --without-hashes --dev
|
||
```
|
||
|
||
輸出的`requirements.txt`內容:
|
||
|
||
```
|
||
black==22.3.0; python_full_version >= "3.6.2"
|
||
click==8.1.2; python_version >= "3.7" and python_full_version >= "3.6.2"
|
||
colorama==0.4.4; python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows"
|
||
...
|
||
```
|
||
|
||
雖然長得有點不一樣,但這個檔案確實是可以`pip install`的。
|
||
|
||
從這裡也可以看出前面提及的「區分套件安裝區塊」的價值——有些時候並不需要輸出開發專用套件。
|
||
|
||
`poetry export`所有參數用法與說明,請參考[文件](https://python-poetry.org/docs/cli/#export)。
|
||
|
||
此時專案目錄結構如下:
|
||
|
||
```
|
||
poetry-demo
|
||
├── poetry.lock
|
||
├── pyproject.toml
|
||
└── requirements.txt
|
||
|
||
0 directories, 3 files
|
||
```
|
||
|
||
### 小結:Poetry 常用指令清單
|
||
|
||
算來算去,Poetry 的常用指令主要有下面幾個:
|
||
|
||
- `poetry add`
|
||
- `poetry remove`
|
||
- `poetry export`
|
||
- `poetry env use`
|
||
- `poetry shell`
|
||
- `poetry show`
|
||
- `poetry init`
|
||
- `poetry install`
|
||
|
||
其中一半,單一專案可能只會用個一兩次而已,比如`init`、`install`和`env use`,實際上需要學習的指令並不多。
|
||
|
||
那麼,只要知曉這些指令,就可以順利運用 Poetry 了嗎?可能是,也可能否,所以我下面還會再補充 Poetry 的常見使用情境與操作方式,讓你接納 Poetry 的阻力可以進一步下降!
|
||
|
||
---
|
||
|
||
## Poetry 常見使用情境與操作 QA
|
||
|
||
這部分會以「**使用場景**」的角度切入,介紹 Poetry 應用情境與操作說明,還包括一些自問自答:
|
||
|
||
1. [新增專案並使用 Poetry](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E4%B8%80%E3%80%81%E6%96%B0%E5%A2%9E%E5%B0%88%E6%A1%88%E4%B8%A6%E4%BD%BF%E7%94%A8-Poetry)
|
||
2. [現有專案改用 Poetry](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E4%BA%8C%E3%80%81%E7%8F%BE%E6%9C%89%E5%B0%88%E6%A1%88%E6%94%B9%E7%94%A8-Poetry)
|
||
3. [在別台主機回復專案狀態](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E4%B8%89%E3%80%81%E5%9C%A8%E5%88%A5%E5%8F%B0%E4%B8%BB%E6%A9%9F%E5%9B%9E%E5%BE%A9%E5%B0%88%E6%A1%88%E7%8B%80%E6%85%8B)
|
||
4. [我想要重建虛擬環境](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%9B%9B%E3%80%81%E6%88%91%E6%83%B3%E8%A6%81%E9%87%8D%E5%BB%BA%E8%99%9B%E6%93%AC%E7%92%B0%E5%A2%83)
|
||
5. [為什麼我不在 Docker 環境中使用 Poetry?](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E4%BA%94%E3%80%81%E7%82%BA%E4%BB%80%E9%BA%BC%E6%88%91%E4%B8%8D%E5%9C%A8-Docker-%E7%92%B0%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8-Poetry%EF%BC%9F)
|
||
6. [我可以使用自己習慣的 virtualenv 嗎?](https://blog.kyomind.tw/python-poetry/?fbclid=IwAR0PsmydXgpKzpbAWzJeK2ze7o0uXgGHn24xUvTCjo9fQQGGhcpnkPBG9n8#%E5%85%AD%E3%80%81%E6%88%91%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E8%87%AA%E5%B7%B1%E7%BF%92%E6%85%A3%E7%9A%84-virtualenv-%E5%97%8E%EF%BC%9F)
|
||
|
||
### 一、新增專案並使用 Poetry
|
||
|
||
這是最理想的狀態,沒有過去的「包袱」,可謂是最能輕鬆採用 Poetry 的情境。
|
||
|
||
使用順序不外乎是:
|
||
|
||
1. `poetry init`:初始化,建立`pyproject.toml`。
|
||
2. `poetry env use python`:建立專案虛擬環境並使用。
|
||
3. `poetry shell`:進入專案但虛擬環境還未啟動,以這個指令啟動。如果使用本指令時虛擬環境**尚未建立或已移除**,則會**直接自動幫你建立虛擬環境**並使用。
|
||
4. `poetry add`:新增套件並寫入虛擬環境。必要時使用`-D`參數新增至 dev 區塊。
|
||
5. `poetry remove`:移除套件,若是移除 dev 區塊的套件,需要加上`-D`參數。
|
||
|
||
這部分和前面內容沒有差別,因為前面內容就是以全新專案作為基礎。
|
||
|
||
### 二、現有專案改用 Poetry
|
||
|
||
極為常見的需求,但並沒有很正式的做法,因為不存在`poetry import`之類的指令。
|
||
|
||
首先要考量的就是:要怎麼把`requirements.txt`的所有項目加到`pyproject.toml`中呢?經過一番 Google,基本上[只能土法煉鋼](https://stackoverflow.com/questions/62764148/how-to-import-requirements-txt-from-an-existing-project-using-poetry):
|
||
|
||
```
|
||
cat requirements.txt | xargs poetry add
|
||
```
|
||
|
||
然而這樣做是有可能遇到一些問題的,因為 Poetry **對套件的版本衝突比較敏感**,所以即便用`pip install -r requirements.txt`都能正常安裝,透過上述指令的遷移過程卻仍有機會出現錯誤。
|
||
|
||
那怎麼辦?只能照著錯誤訊息手動修正`requirements.txt`中的套件版本。
|
||
|
||
只能說這個「**手動 import**」做法實在是不得已,因為我們最早介紹`pyproject.toml`時有提到,`poetry add`只會在`pyproject.toml`中寫入「主套件」,但這樣的 import 方式相當於把`requirements.txt`中的**所有套件,都當作主套件**來`add`了!
|
||
|
||
畢竟在`requirements.txt`中**無從區分**主套件與依賴套件,都是「一視同仁」地列出。
|
||
|
||
但如此做法也讓專案的套件**失去主從之分**,這樣會有什麼**壞處**?日後要移除主套件時,**需要花額外的心力去區分主從**(因為僅僅移除依賴套件**並不會有移除效果**),比如使用`poetry show --tree`去一個一個檢視,終究是件麻煩事。
|
||
|
||
完成轉換後,為保險起見,建議透過新的`pyproject.toml`來重建一個虛擬環境。
|
||
|
||
### 三、在別台主機上重現專案的 Poetry 虛擬環境
|
||
|
||
這也是非常常見的需求。
|
||
|
||
第一步當然是`git clone`專案,此時專案中已經有 Poetry 所需的必要資訊了——也就是`pyproject.toml`和`poetry.lock`。
|
||
|
||
你還缺少的僅僅是虛擬環境。如果是全新的主機,則還得先安裝、設定好 Poetry。
|
||
|
||
確定 Poetry 可正常使用後,移至專案目錄底下,依序執行指令:
|
||
|
||
1. `poetry env use python`:建立專案虛擬環境並使用。如果你懶得打這麼長的指令,直接`poetry shell`也是可以。此時我們會有一個「**空的**」虛擬環境。
|
||
2. `poetry install`:因為是舊專案,不需要`init`,會直接依`poetry.lock`記載的套件版本安裝到虛擬環境中!類似`npm install`。
|
||
|
||
### 四、我想要重建虛擬環境
|
||
|
||
在使用專案內虛擬環境方案,也就是`.venv`的前提下,想要刪除這個虛擬環境並加以重建,也不需要使用`poetry env remove python`指令了,因為會出錯。
|
||
|
||
還有更簡單暴力的方式,是什麼呢?——直接刪除`.venv`資料夾即可。
|
||
|
||
然後再`poetry env use python`或`poetry shell`建一個新的就好。
|
||
|
||
### 五、為什麼我不在 Docker 環境中使用 Poetry?
|
||
|
||
因為啟動容器後需要先安裝 Poetry 到全域,或打包一個帶有 Poetry 的 image,兩者都會**增加新的耦合與依賴**,我覺得並不妥當。
|
||
|
||
所幸 Poetry 依舊可以輸出`requirements.txt`,Docker 部署環境就繼續使用這個舊方案即可,而且 Poetry 本來主要就是用於「開發」時的套件管理,對部署差別不大。
|
||
|
||
### 六、我可以使用自己習慣的 virtualenv 嗎?
|
||
|
||
當然可以。
|
||
|
||
不過我本來也繼續使用`pyenv`的`virtualenv`,但兩者有時候也是會小小打架,後來還是索性用 Poetry 的虛擬環境就好。
|
||
|
||
一個專案對應一個虛擬環境,應該還是比較簡潔的做法,我的觀察啦!😎
|
||
|
||
---
|
||
|
||
## 結語:井然有序的複雜
|
||
|
||
總的來說,Poetry 是一款優秀的套件管理工具,但並不像 pip 那般簡單、好上手。
|
||
|
||
使用 Poetry 來管理專案的套件與虛擬環境,需要一定的學習成本,但帶來的效益還是相當可觀的,尤其在你希望能夠乾淨且安心地移除套件之際,可謂莫它莫屬。
|
||
|
||
所以,別再猶豫,從今天起,加入 Poetry 的行列吧!
|
||
|
||
### 參考
|
||
|
||
- [https://python-poetry.org/docs/](https://python-poetry.org/docs/)
|
||
- [https://github.com/python-poetry/poetry](https://github.com/python-poetry/poetry)
|
||
- [https://github.com/python-poetry/poetry/issues/3248](https://github.com/python-poetry/poetry/issues/3248)
|
||
- [https://github.com/python-poetry/poetry/issues/5185](https://github.com/python-poetry/poetry/issues/5185)
|
||
- [Python - 取代 Pipenv 的新套件管理器 Poetry](https://note.koko.guru/posts/using-poetry-manage-python-package-environments)
|
||
- [相比 Pipenv,Poetry 是一個更好的選擇](https://greyli.com/poetry-a-better-choice-than-pipenv/)
|
||
- [pip, pipenv 和 poetry 的選擇](https://shazi.info/pip-pipenv-%E5%92%8C-poetry-%E7%9A%84%E9%81%B8%E6%93%87/)
|
||
- [Dependency Management With Python Poetry](https://realpython.com/dependency-management-python-poetry/)
|
||
- [Ep 15. 和 PyPA 的成員聊聊 Python 開發工作流](https://pythonhunter.org/episodes/ep15)
|
||
- [Python - Poetry](https://blog.jihongo.com/posts/2022/06/04/python-poetry/)
|
||
|
||
**相關文章**
|
||
|
||
- ### [告別 Anaconda:在 macOS 上使用 pyenv 建立 Python 開發環境](https://blog.kyomind.tw/pyenv-setup/ "告別 Anaconda:在 macOS 上使用 pyenv 建立 Python 開發環境")
|
||
- ### [Ubuntu 安裝使用 pyenv + pyenv-virtualenv](https://blog.kyomind.tw/ubuntu-pyenv/ "Ubuntu 安裝使用 pyenv + pyenv-virtualenv")
|
||
- ### [pyproject.toml 介紹 + VS Code 整合 Black、yapf、isort 教學](https://blog.kyomind.tw/pyproject-toml/ "pyproject.toml 介紹 + VS Code 整合 Black、yapf、isort 教學") |