งูต้นฉบับหยิบแฟ้มจากกล่องแพ็กเกจผ่านช่องทางที่กำหนด แทนการเดาตำแหน่งบนชั้น

Python importlib.resources.files: อ่านไฟล์ใน package โดยไม่เดา path

พี่โชว์Engineering

ไฟล์ template ที่มากับ package อาจอยู่ใน source tree, wheel หรือ zip import การประกอบ path จาก __file__ จึงผูกกับรูปแบบติดตั้งโดยไม่จำเป็น importlib.resources.files คืน Traversable ให้เดินและอ่าน resource ผ่าน import system ครับ

WHY: ขอของจากแพ็กเกจ ไม่เดาชั้นวาง

เหมือนยื่นเลขพัสดุให้คลังสินค้า เราบอก package กับชื่อ resource แล้วให้ loader จัดการว่าของอยู่ในชั้นจริงหรือ archive วิธีนี้ทำให้ test และ distribution ใกล้พฤติกรรม production มากขึ้น

import importlib
import importlib.resources
import pathlib
import sys
import tempfile
import unittest


def read_template(package: str, name: str) -> str:
    root = importlib.resources.files(package)
    resource = root.joinpath(name)
    if not resource.is_file():
        raise FileNotFoundError(name)
    return resource.read_text(encoding="utf-8")


class ResourceTests(unittest.TestCase):
    def test_reads_from_importable_package(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            package = pathlib.Path(tmp, "demo_assets")
            package.mkdir()
            package.joinpath("__init__.py").write_text("", encoding="utf-8")
            package.joinpath("welcome.txt").write_text("สวัสดี", encoding="utf-8")
            sys.path.insert(0, tmp)
            try:
                importlib.invalidate_caches()
                self.assertEqual(read_template("demo_assets", "welcome.txt"), "สวัสดี")
            finally:
                sys.path.remove(tmp)
                sys.modules.pop("demo_assets", None)


if __name__ == "__main__":
    unittest.main()

HOW: Traversable ไม่จำเป็นต้องเป็น pathlib.Path

เมธอด joinpath, is_file, read_text ใช้กับ resource ได้โดยไม่ขอ path จริง ถ้า library ภายนอกต้องการ filesystem path ให้ใช้ importlib.resources.as_file() เป็น context manager เพราะ loader อาจต้องแตกไฟล์ชั่วคราวและ path นั้นหมดอายุเมื่อออกจาก context

ชื่อ resource ควรมาจาก allowlist ไม่รับ ../ จากผู้ใช้แล้วหวังว่า API จะเป็น sandbox ตรวจ manifest ของ wheel ด้วยว่า data file ถูกแพ็กจริง และอย่าใส่ secret ลง package เพราะใครติดตั้งก็อ่านได้ Resource เหมาะกับ template, schema และข้อมูลคงที่

Recap

  • อ้าง package + resource แทน path จาก __file__
  • ใช้ Traversable API โดยไม่สมมติว่าเป็นไฟล์จริง
  • ใช้ as_file() เมื่อจำเป็นและอยู่ใน context
  • allowlist ชื่อ resource และตรวจ wheel manifest
  • package data ต้องไม่เป็น secret