Use Pyenv and Pipenv to build clean Python Environment

這一篇文章受到了 PyConTW 總召 TP 的 PyConTW 2018 Talk - 這樣的開發環境沒問題嗎? 的啟發,以及在 Taipei.py x PyConTW 的 Sprint 裡面看到 Pipenv 裡面好多好棒棒的功能(以及心中對開發環境的潔癖作祟),所以才寫了這篇文章記錄要怎麼做出一個 Python 開發環境潔癖重症患者喜愛的開發環境。

前言

Warning: 需要對 Unix-like OS 有基礎認知,不然你可能就會找不到你的 Python 在哪裡。

其實很多作業系統都會自帶 Python,大部分的 Linux(e.g. Ubuntu)與 Mac OS 都有內建 Python 2.x,然而很多 System software 都會高度依賴 Python,所以如果把系統內建的 Python 刪掉,會很好玩哦

因此,我們並不打算把原生的 Python 刪掉,我們傾向秉持著互不侵犯原則,我不用系統內建的 Python,系統內建的 Python 也別想來擾亂我的開發環境的概念。

而這裡敘述只以 Mac 作為範例,Ubuntu 可以參考

我們會需要使用的工具有這兩個:

而稍後的章節:自動建立 pyenv 與 pipenv 結合的隔離式開發環境的程式碼則位於以下專案:

推薦 pyenvpipenv 都使用 Homebrew 安裝,如果你使用 zsh 並且是 oh-my-zsh 的愛用者,又已經透過 Homebrew 安裝了 pyenv 的話,千萬不要用 pyenv 的 plugin,否則你會有兩個 pyenv,雖然在操作 shell 時體感很好,還提供了版本號自動補全功能,但每次開啟一個新的 Terminal 都會需要等待 pyenv 載入,大概耗時會兩秒,超慢,別用。

關於 pyenv 與 pipenv 的操作說明

可以先嘗試以下指令來試試 pyenvpipenv 是怎麼樣的東西。

pyenv 的使用簡介

pyenv 的常用指令如下,global 與 local 的差異是當前資料夾使用的 python 版本,但我通常只使用 global。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 列出所有可以安裝的 Python 版本
$ pyenv install --list
Available versions:
2.1.3
2.2.3
2.3.7
... 後略

# 下載 CPython 3.6.5 Source Code 編譯並安裝
$ pyenv install 3.6.5

# 列出所有可以使用的 Python 版本(星號代表使用中)
$ pyenv versions
system
* 2.7.15 (set by /Users/aweimeow/.pyenv/version)
3.6.5

# 切換到 3.6.5 版本(global)
$ pyenv global 3.6.5

# 切換到 2.7.15 版本(local)
$ pyenv local 2.7.15
```

## pipenv 的使用簡介

pipenv 的常用指令如下,基本上就跟 pip 完全一樣,如果有需要的情況可以使用 `pipenv shell` 達到像是 `virtualenv` 的 `. venv/bin/activate` 效果。

```bash
# 為當前目錄啟動一個 Python3.6 的 virtual environment
$ pipenv --python 3.6

# 安裝所有目前開發環境的 dependencies(抓 requirements.txt 或 Pipfile)
# 如果目前專案是使用 requirements.txt,會轉換產生出 Pipfile
$ pipenv install --dev

# 從 PyPI.org 下載安裝 colorlog
$ pipenv install colorlog --dev

# 從 localpath 安裝 colorlog
$ git clone https://github.com/borntyping/python-colorlog
$ pipenv install ./python-colorlog --dev

# 從 GitHub 安裝 colorlog
$ pipenv install git+https://github.com/borntyping/python-colorlog --dev

# 在 pipenv 當中執行命令
$ pipenv run where python

# 進入 pipenv 的 virtual environment 當中
$ pipenv shell

自動建立 pyenv 與 pipenv 結合的隔離式開發環境

自動部屬腳本只能在 Ubuntu 環境使用,其他作業系統要稍微改動一下

在開發的過程中,我很常在不同的機器之間切換開發,重複部屬開發環境的行為就如同不寫重複的程式碼一般,我們必須把工作簡化再簡化,Make your life easier。能躺著就不坐著的肥宅精神也到了我的工程師魂當中,因此我寫了腳本來自動部屬以下 手動建立隔離式開發環境 的程式碼,使用上只要一條指令就能把整個熟悉的開發環境建立起來了:

1
curl https://aweimeow.tw/python | bash

再建立好之後,可以自由選擇想要編譯的 python 版本,並透過這個指令來編譯安裝:

1
2
3
4
5
# pyenv install <version>
$ pyenv install 3.6.5

# 列出目前擁有的 python 版本,星號則為目前使用的版本
$ pyenv versions

並透過以下指令來切換到該版本,以及呼叫修改過的 pyenv 腳本自動建立 pipenv 虛擬環境:

1
2
# pyenv change <version>
$ pyenv change 3.6.5

手動建立 pyenv 與 pipenv 結合的隔離式開發環境

但因為我不想要安裝任何 package 在系統原生的 Python 當中,甚至連使用都不想使用它的話,那就可以使用 pyenv + pipenv 達到組合技,使自己平常使用的 Python 可以成為獨立於系統內建 Python 之外的另一隻程式。只需要設定好環境變數,讓使用到的 Python 是自己建立的 Symbolic Link 就可以了。

因此我們要先針對 ~/.zshrc 做 configuration,設定 PATH 把自己家目錄下的 ~/.local/bin 放在優先權最高的位置:

1
export PATH="$HOME/.local/bin:$PATH"

而我從 Homebrew 安裝的 pyenv 預設路徑是 ~/.pyenv,可以在路徑當中找到所有安裝的 Python 版本,這裡面有一個 shims 資料夾,shims 的功能就是作為 Symbolic Link,若我們能將它設定成環境變數 PATH ,使用到的 pythonpip 就都會是 shims 資料夾裡面的 shell script 了。但這個並不符合我們的需求,我們希望可以基於 pyenvshims 之上,建立出 virtualenv,並且實際使用到的 pythonpip 是 virtualenv 裡面的 executable,如此一來即使我們自己編譯的 Python 環境被弄髒,也只需要把 virtualenv 砍掉重建就可以回到乾淨的 Python 了,不需要重新編譯安裝。

所以我又建立了一個 ~/.pipenv 作為存放所有 virtualenv 的資料夾,並寫了一個 script 幫 pyenv 新增了一個自己定義的 change function。pyenv change 會做的事情有:

  1. 檢查有沒有指定的 Python 版本
  2. 切換到該版本
  3. 若該版本的 virtual environment 還不存在,就用 pipenv 建立一個
  4. 把 virtualenv 的 python 與 pip link 到 ~/.local/bin

pyenv 的 patch 腳本

身為工程師,怎麼可以比別人還勤勞,因此我寫了一個簡單的 script 來 patch pyenv,在此我們只需要將以下 script 存在 ~/.local/bin/pyenv ,並執行 chmod +x ~/.local/bin/pyenv 就可以了。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env bash
set -e

program="${0##*/}"

LOCALDIR="$HOME/.local";
SOURCEDIR="$LOCALDIR/src";
BINDIR="$LOCALDIR/bin";

export PYENV_ROOT="$SOURCEDIR/pyenv"

if [[ "$1" = "change" ]]; then
$SOURCEDIR/pyenv/bin/pyenv global $2;
if [[ "$?" -ne "0" ]]; then
echo "Assigned version not install yet in Pyenv.";
exit;
fi

cwd=$(pwd)
directory="$SOURCEDIR/pipenv/$2";

if [[ ! -d $directory ]]; then
mkdir -p $directory && cd $directory;
pythonbin="$SOURCEDIR/pyenv/versions/$2/bin/python";
mkdir -p $SOURCEDIR/pipenv/$2 && cd $SOURCEDIR/pipenv/$2;
pipenv --python $pythonbin;
fi

cd $SOURCEDIR/pipenv/$2/.venv/bin/;

for file in "python" "pip" "bpython" "git-review" "jupyter"; do
rm -f $BINDIR/$file;
if [ -e $file ]; then
ln -s $(pwd)/$file $BINDIR/$file;
fi
done

echo "Python Environment checkout to $2 ($directory)";

cd $cwd;
else
# If parameter is not change, execute it
exec "$SOURCEDIR/pyenv/bin/pyenv" "[email protected]";
fi

此時,你已經可以使用 pyenv change 2.7.15 建立出一個 2.7.15 且在 virtual environment 中的 Python 了,可以使用以下 code 來驗證:

1
2
3
4
5
6
7
>>> import sys
>>> import os
>>> sys.executable
'/Users/aweimeow/.local/bin/python'
>>> os.system('ls -al {}'.format(_))
lrwxr-xr-x 1 aweimeow staff 47 6 4 12:04 /Users/aweimeow/.local/bin/python -> /Users/aweimeow/.pipenv/2.7.15/.venv/bin/python
0

可以發現,我們執行的 Python 是 /Users/aweimeow/.local/bin/python,且這個是一個 symbolic link,指向到 .pipenv 下的 2.7.15 虛擬環境了。

開發環境完全隔離的功能性驗證

我們現在已經可以使用 pyenv change 3.6.5 來方便的切換 python 版本了,那這樣還能夠在 project 目錄裡面建立一個又獨立的 environment 嗎?可以!

因為 pipenv 最多 support 兩層 virtualenv,所以我們還是可以在專案目錄下建立 virtual environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ mkdir py_vir_env_test && cd $_
$ pipenv --python ~/.local/bin/python
Creating a virtualenv for this project…
Using /Users/aweimeow/.local/bin/python (2.7.15) to create virtualenv…
⠋Running virtualenv with interpreter /Users/aweimeow/.local/bin/python
Using real prefix '/Users/aweimeow/.pyenv/versions/2.7.15'
New python executable in /Users/aweimeow/py_vir_env_test/.venv/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location: /Users/aweimeow/py_vir_env_test/.venv

$ python -c 'import sys; print(sys.executable)'
/Users/aweimeow/.local/bin/python

# 在 pipenv 裡面執行的結果
$ pipenv run python -c 'import sys; print(sys.executable)'
/Users/aweimeow/py_vir_env_test/.venv/bin/python

大概就是這樣子,Enjoy with pyenv & pipenv ( ´ ▽ ` )ノ