完整移除 pyenv

我是用 homebrew 安裝 pyenv ,之前是用 pyenv 在管理 python 環境,但現在進入 AI 時代,AI 反倒不知道要載入哪個環境。

這邊後來是看到可以用 pipx 來安裝跟管理這種獨立的工具,再加上 uv 管理 python 專案更為方便,為此,乾脆就移除 pyenv 了。

若要先備份每個 python 套件,可以用以下腳本來進行

mkdir -p ~/pyenv-pip-backup

for venv in $(pyenv virtualenvs --bare | grep -v '/'); do
  echo "匯出 $venv ..."
  "$PYENV_ROOT/versions/$venv/bin/pip" freeze > ~/pyenv-pip-backup/"$venv".txt
done

這樣會把所有安裝過的套件備份到 ~/pyenv-pip-backup 目錄下。

然後用 homebrew 移除 pyenv。

brew uninstall pyenv

# 若有安裝 pyenv-virtualenv 這個 plugin(也常透過 brew 裝)
brew uninstall pyenv-virtualenv

再刪除 pyenv 檔案。

rm -rf "$(brew --prefix)/opt/pyenv" 2>/dev/null  # 保險起見清一下 opt 連結(通常不存在)
rm -rf ~/.pyenv

清理環境變數,先找看哪個檔案有

grep -n "pyenv" ~/.zshrc ~/.zprofile ~/.bash_profile ~/.bashrc ~/.profile 2>/dev/null

通常內容是

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

把這些刪除掉。

最後關閉終端機,重新開,讓環境變數重新載入。

開完以後,用以下方式檢驗,照理說應該就都看不到了。

# 確認指令已消失
command -v pyenv
which pyenv

# 應該回傳「找不到指令」或空白

# 確認目錄已清空
ls -la ~/.pyenv 2>&1

# 應顯示 No such file or directory

# 確認 PATH 中已無 pyenv 蹤跡
echo $PATH | tr ':' '\n' | grep pyenv

# 應該沒有任何輸出

# 確認 brew 也不再列出
brew list pyenv 2>&1

簡化路徑

路徑太長,想要縮短到 80 個字元以內,又要可以辨識,唯一找到的工具是 https://github.com/chrissound/SodiumSierraStrawberry

但這工具執行起來有問題,想要重新編譯,對 haskell 又不熟,只好放棄。

好在找到這篇:Path shortener for GUI application ,裏面提到可以用 Python3 的 pathlib 來做,於是就順手改了一下,做出自己要的工具

#!/usr/bin/env python3

import argparse
import sys
import pathlib

def cut_path(path, limit):
    if len(path)>limit:
      parts = list(pathlib.PurePath(path).parts)
      # print(parts)
      #if len(parts) >= 4:
      #    parts [2:-1] = ['...']
      new_parts = []
      for part in parts[:-1]:
        if len(part)>4:
          new_parts.append(part[:4] + "..")
        else:
          new_parts.append(part)
      new_parts.append(parts[-1])
      #print(new_parts)
      new_path = str(pathlib.PurePath(*new_parts))
      #print(len(new_path))
      if len(new_path) > limit:
        parts[2:-1] = ['...']
        return pathlib.PurePath(*parts)
      return new_path
    else:
      return path


def main():
    parser = argparse.ArgumentParser(
      prog=sys.argv[0],
    )
    parser.add_argument('-l', '--limit', type=int, default=76)
    parser.add_argument('filename')

    args = parser.parse_args()

    print(cut_path(args.filename, args.limit))


if __name__ == "__main__":
    main() 

基本運作是這樣

  1. 超過指定字元數目就做縮減,沒超過就直接回傳。
  2. 縮減的作法:使用 Pathlib 的 PurePath.parts() 把路徑拆成一段一段的,然後用名稱去做判斷,超過 4 個字元就把後面的改為 “…” ,然後產出新的字串。
  3. 若 2 的字串還是超過指定字元數目,就直接把其中一部份都刪除掉,改為 “…”

結果大約會是這樣

/home/user1/aaa.txt
coll../ansi../gov/twgc../role../veri../task../main.yml
collection/ansible_collections/.../main.yml

能夠這樣用 Python3 快速的弄出個小工具,真的是蠻開心的,Python3 真的自帶很多好東西可用。

好用的新Python函式庫

從 It’s Time to Say Goodbye to These Obsolete Python Libraries 看到的幾個好用的新函式庫跟新功能,雖說用新的怕舊的 Python 不能用,但這些新的函式庫或新功能其實也出蠻長一段時間了,現在大部分的 Linux 發行版也都有跟到。

摘錄如下

Pathlib

處理路徑的函式庫,直接看看這幾種用法,就能體會,比起之前的 os.path 直覺太多。

from pathlib import Path

readme = Path("README.md").resolve()

print(f"Absolute path: {readme.absolute()}")
# Absolute path: /home/martin/some/path/README.md
print(f"File name: {readme.name}")
# File name: README.md
print(f"Path root: {readme.root}")
# Path root: /
print(f"Parent directory: {readme.parent}")
# Parent directory: /home/martin/some/path
print(f"File extension: {readme.suffix}")
# File extension: .md
print(f"Is it absolute: {readme.is_absolute()}")

etc = Path('/etc')

joined = etc / "cron.d" / "anacron"
print(f"Exists? - {joined.exists()}")

Secrets

處理密碼的函式庫,簡單很多,而且語意很清楚。

import secrets

value = secrets.token_bytes(length)
value = secrets.token_hex(length)

dataclass

若之前有使用過 named tuple 的話,會覺得這個函式庫很好用,而且語意更清楚。

from dataclasses import dataclass

@dataclass()
class User:
    name: str
    surname: str

u = User("John", "Doe")

f-string

Python 從最早期的 “%s” % (name, ) 再到 “{}”.format(name) ,再到 f-string,f-string 讓格式化字串更為清楚,也不容易搞錯。

name = "John"
print(f"Hello {name}!")

結論

定期升級語法,可以帶來效能提升,也可以讓程式更具可讀性,是蠻值得安排時間去逐步更新的。

同場加映:Python 3.11–5個新功能 – anfinance – Medium

pip-audit

github 會針對套件去做安全檢核並發出通知或是 pull request,如果本地端想要做這件事,該怎麼做呢?

剛好前幾天看到這篇:How to find third-party vulnerabilities in your Python code ,文章裡就介紹了 pip-audit 這個工具。使用這個工具,就可以對使用的套件來做安全檢核,並給予建議。

安裝方法很簡單,用 pip 安裝就可以

pip install --upgrade pip-audit pip

安裝完成後,就可以使用 pip-audit 指令了。

要對使用的套件檢查,可以用

pip-audit -r requirements.txt

檢查需要花一段時間,檢查後的結果大致會類似這樣子:

Found 1 known vulnerability in 1 package                                              
Name    Version ID             Fix Versions
------- ------- -------------- ------------
urllib3 1.25.11 PYSEC-2021-108 1.26.5

你會看到有幾個已知漏洞,套件的名稱跟修正的版本。

若要即時的修正,pip-audit 也可以做修正。

pip-audit --fix

要注意的是,這個修正只是幫你安裝已經沒有漏洞的版本,requirements.txt 裡還是要自己去修正。

後續思考:

  1. 可以在 CI 流程裡使用 pip-audit 來做安全檢核,避免使用到有漏洞的版本。
  2. 使用 poetry 的話,可以把 pip-audit 列為 dev dependencies ,就不需要在正式環境裡也裝上這工具了。

Python inspect

之前在用 c# 時,可以使用 reflection 來查找類別可以使用的方法或屬性,甚至是函式庫裡有什麼類別、函式可以使用。在 Python 可以用 inspect 模組來達成這件事情:How to list all functions in a Python module?

例如要列出模組裡所有的函式,可以這樣用:

from inspect import getmembers, isfunction
from somemodule import foo

print(getmembers(foo, isfunction))

或是要找出名字裡有 castle 的成員,像是函式、類別等等的:

import inspect
import example

for name, data in inspect.getmembers(example):
    if 'castle' not in name:
        continue
    print('{} : {!r}'.format(name, data))

或是要找類別名稱是 XXXManager 的類別:

import inspect
import example

for name, data in inspect.getmembers(example, inspect.isclass):
    if name.endswith('Manager'):
      print('{} : {!r}'.format(name, data))

要撈 docstring 也可以,更多的用法可以參考 PyMOTW: inspect — Inspect Live Objects

Python 網站上的文件:inspect – Inspect Live Objects

subprocess.check_output 小記

subprocess 是 python 一個很好用的模組,提供執行子程序的各種方式,最常用的就是執行,然後取得輸出結果。

import subprocess

try:
  cmd_output = subprocess.check_output("ls")
except subprocess.CalledProcessError as ex:
  cmd_output = ex.stdout

print("Command output:")
print(cmd_output)

接下來,因為有些指令是之前已經串好,已經加上 pipe 的了,所以會這樣用

cmd_output = subprocess.check_output("ls /usr/bin | grep python")

但你會發現 subprocess.check_output 拋出一個例外

FileNotFoundError: [Errno 2] No such file or directory: 'ls /usr/bin | grep python': 'ls /usr/bin | grep python'

這到底怎麼回事?明明就有 ls 這個執行檔?

原因很簡單,因為 subprocess.check_output 是把第一個參數視為一個檔案,所以有 pipe 的情況,他是不認可的。這時候可以加上 shell=True,讓 subprocess.check_output 知道執行第一個參數時,要先啟動 shell 來執行第一個參數裡的命令。

cmd_output = subprocess.check_output("ls /usr/bin | grep python", shell=True)

在取得輸出結果以後,cmd_output 是一個 byte 型態的變數,換言之,他不是字串。這時需要 decode 幫忙,轉換為字串:

cmd_output = cmd_output.decode('utf-8')

最後要分享的是將錯誤輸出到 null 裝置的方法,一般 bash script 裏面會用這樣的方式來將錯誤導向到 null 裝置,以避免出現不必要的輸出:

ls /usr/bin/foo 2>/dev/null

在呼叫 subprocess.check_output 時,可以帶入一個參數 stderr,而無需把 >/dev/null 寫在第一個參數裡。

cmd_output = subprocess.check_output("ls /usr/bin/foo", stderr=subprocess.DEVNULL, shell=True)

以上是自己在把 shell script 轉換為 python 時遇到的幾種狀況,因為已經遇到幾次,總是會多花一點時間回想,這次在這邊紀錄下來,以免之後又忘掉。

Django site framework

之前專案都是使用 cookiecutter + cookiecutter-django 來產生的,前一陣子在它產生出來的設定裡看到 SITE_ID 以及註解,就發現了 Django site framework 。

Django site framework 裡的 site ,就有點像是 virtual host 裡的 host,簡單的說,可以做到以下事情:

  • Virtual host:Django 可以有能力去判斷網址裡的網域,來決定如何處理。
  • 不同網站入口,但共用資料庫

啟用方法

基本上 site framework 已經內建在 Django 裡了,所以只要啟用就可以

  1. 在 settings 的 INSTALLED_APPS 加入 “django.contrib.sites”
  2. 增加 SITE_ID  (選填,可以加這個設定,也可以不加)

執行 migrate python manage.py migrate

接著利用 admin 後台或者是 shell 去新增 site。

功能

有提供以下功能

  • 物件 (model) 跟指定的 site 關連
  • View 可以依據 site 來做出不同的處理跟回應
  • 多個 site,但只使用指定的 site
  • 自動依照網址內的網域來判斷,並將 site 放到 request 裡
物件 (model) 跟指定的 site 關連

site framework 本身有建立 model ,也就是說有建立資料表格,所以其實是可以讓物件模型跟 site 做關連,那麼之後在處理時,就可以依據這個關係,只顯示出跟指定 site 相關連的物件。

# 引用自 django 文件
from django.contrib.sites.models import Site
from django.db import models

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    sites = models.ManyToManyField(Site)

上面的程式碼,就是表示 Article 跟 Site 做多對多的關聯。之後在 View 裡就可以用下面的程式碼撈出跟指定站點相關連的 Article

# 引用自 Django 文件
from django.contrib.sites.shortcuts import get_current_site

def article_detail(request, article_id):
    try:
        a = Article.objects.get(id=article_id, sites__id=get_current_site(request).id)
    except Article.DoesNotExist:
        raise Http404("站點裡沒有文章")
    # ...
View 可以依據 site 來做出不同的處理跟回應

其實上面的程式碼已經就是了,這邊再舉個例子

# 引用自 Django 文件
from django.contrib.sites.shortcuts import get_current_site

def my_view(request):
    current_site = get_current_site(request)
    if current_site.domain == 'foo.com':
        # Do something
        pass
    else:
        # Do something else.
        pass

這邊使用 site framework 提供的 shortcut – get_current_site 來取得目前的 site,然後用 if – else 來做判斷,執行不同的邏輯

多個 site,但只使用指定的 site

那程式裡已經有多個 site,但是想拆分出來,只服務單一個 site 時,可以在 settings 裡指定 SITE_ID,程式裡可以直接引用,例如

from django.conf import settings

def my_view(request):
    if settings.SITE_ID == 3:
        # Do something.
        pass
    else:
        # Do something else.
        pass

這個蠻適合應用在 docker 上,我只要打包好 docker image,之後就可以再利用指定環境變數的方式去讓這個 container 能處理指定的 site

自動依照網址內的網域來判斷,並將 site 放到 request 裡

要完成這個功能,需要在 settings 裡的 MIDDLEWARE 裡,加入 “django.contrib.sites.middleware.CurrentSiteMiddleware”

加入以後,View 裡的 request 就會多出一個 site 的屬性,那在 View 裡,就可以直接使用 request.site

Site framework 內部的運作

依據上面的說明,除了資料庫以外,get_current_site 好像…挺重要的,下面就來繼續挖掘。

我們先從 middleware 開始,CurrentSiteMiddleware 裡面蠻簡單的,只有 override process_request(),裡面只有一行:request.site = get_current_site(request)

!! 又是 get_current_site()

那 get_current_site() 又做了什麼事情?get_current_site() 只判斷 django.contrib.sites 有沒有在 settings INSTALLED_APPS 裡,有的話,就使用 Site.objects.get_current() 來取得目前的 site;沒有的話,改用 RequestSite 類別來判斷,RequestSite 只是一個封裝,封裝出類似 Site 的物件,讓你後續存取可以跟原來的 site 物件一樣。

    if apps.is_installed('django.contrib.sites'):
        from .models import Site
        return Site.objects.get_current(request)
    else:
        from .requests import RequestSite
        return RequestSite(request)

先看 Site.objecs.get_current() ,這函式的實作是在 django/contrib/sites/models.py 裡。裡面也很簡單,先去看 settings 裡有沒有 SITE_ID,有的話,就用 SITE_ID 去查詢資料表格,取出對應的 site;沒有 SITE_ID 或者是 SITE_ID 被判定為 False (‘’, 0 都算是 False),就改使用 _get_site_by_request() 從 request 去做判讀。

_get_site_by_request() 使用 request.get_host() 取出 host,然後解析出 domain/port,再使用 domain 去查詢資料表格,找到對應的 site。

總結

以上,就是 site framework ,為了驗證我對 site framework 的認知,製作了一個小的展示專案,放在 github 上:https://github.com/elleryq/site_framework_demo

檢查 python 套件是否過期

用 pip 就可以檢查了

pip list ——outdated ——format=columns

也可以改用 json 格式輸出

pip list ——outdated ——format=json

但有時會想知道目前裝的版本跟最新版本差多少,這時可以使用 pip-check 這個工具,先安裝

pip install pip-check

進行檢查

pip-check ——cmd=pip ——hide-unchanged

加上 -u 的話,後面會補上升級指令

pip-check ——cmd=pip ——hide-unchanged -u

Visual Studio code snippets

一直都沒認真研究 vscode snippets 怎麼弄
今天去看了以後,發現蠻簡單的:

  • 啟用:選到 Preferences > Settings ,在上方的搜索框輸入 “editor.tabcompletion”,把 Tab Completion 選為 on 或 onlySnippets 就可以。在輸入 prefix (前置字串)後,按下 tab ,就會出現 snippets 讓你選擇。
  • 建立 snippets:
    • 手動建立:選到 Preferences > User snippets ,此時會要問你要編輯哪種類型檔案的 snippets,選好以後,會開啟一個 json 檔案,檔案裡面有註解說明,依照說明去編輯就可以了。編輯好,會立即生效。
    • 快速建立:安裝 Snippet Creator 這個 Extension (尋找 ryanolsonx.snippet-creator),安裝好以後,先選取你要建立 snippet 的文字,然後按下 `ctrl + shift + p` ,依照提示輸入 prefix, description 以後,就可以了。
  • 現成的 snippets:事實上,也有很多人把自己建立的 snippet 分享為 extension,供大家安裝。像 Python 就可以安裝 “Python snippets” (尋找 frhtylcn.pythonsnippets),或是 “Python extended” (尋找 tushortz.python-extended-snippets),或是 “Django” (尋找 bigonesystems.django),安裝好以後,就可以使用了。至於該 extension 有提供哪些 snippets,就得自己去挖 extension 的說明檔或是原始碼了,對,這個意思就是要稍微記一下有哪些 prefix (前置字串),否則就都沒用到,等於是白裝。

參考資料: