簡化路徑

路徑太長,想要縮短到 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 (前置字串),否則就都沒用到,等於是白裝。

參考資料:

Django開發者常犯的7個錯誤

從這篇 7 common mistakes that django developers make 整理出來的

  • Reinventing the wheel
    • 儘量找人家已經寫好的 package 來用,不要沒事自己在那邊重新刻
  • Monolith application structure
    • 要切 app ,該切就切。
    • 我自己目前有比較 confuse 的是,有用到其他 app 的東西時,該怎麼降低耦合度,又能使用方便,這中間去找到一個平衡點。這邊要等以後再來慢慢想怎麼做會更好了。
  • Writing fat representations and thin models
    • View 不要寫太多邏輯
    • 把邏輯抽到 module 或是 model 裡,讓邏輯統一,才能儘量重複使用。
  • Too many queries per view, or unoptimized queries
    • 一個 view 有太多資料庫查詢或是沒有將查詢最佳化,這個會導致速度變慢。
    • 我建議這邊同時可以考慮儘量使用 cache ,來減輕資料庫負擔。
  • Redundant model fields
    • 思考是不是要有真實的資料庫欄位,思考是不是可以使用 property 來做到同樣的事情。
  • Not adding indexes on models
    • 常用來查詢的欄位 (放在 filter 裡的) 記得加 index :db_index=True,該加就加。
  • Inconsistent data validation
    • model 的檢查跟 form 的檢查要一致。