สร้าง CLI Python จัดการไฟล์ด้วย Context Manager และ Test ง่ายๆ

สวัสดีครับ

วันนี้ผมจะพาเพื่อนๆ มาลองสร้างเครื่องมือ CLI (Command Line Interface) ง่ายๆ ด้วย Python กันนะครับ ที่จะช่วยจัดการไฟล์ และที่สำคัญ เราจะใช้ context-manager เข้ามาช่วยให้โค้ดของเราปลอดภัยขึ้น แล้วก็มีการเขียน Test ครอบคลุมด้วยครับ

เริ่มต้นด้วย CLI ง่ายๆ

เราจะเริ่มจากการสร้างฟังก์ชันง่ายๆ สำหรับอ่านไฟล์นะครับ

import argparse

def read_file_content(filepath):
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        print(f"Content from {filepath}:\
{content}")
        return content
    except FileNotFoundError:
        print(f"Error: File not found at {filepath}")
        return None
    except Exception as e:
        print(f"An error occurred: {e}")
        return None

def main():
    parser = argparse.ArgumentParser(description="A simple file reader CLI tool.")
    parser.add_argument("filepath", type=str, help="Path to the file to read.")
    args = parser.parse_args()
    read_file_content(args.filepath)

if __name__ == "__main__":
    main()

ตรงนี้ครับ เราใช้ argparse เพื่อให้โปรแกรมของเราสามารถรับค่าจาก command line ได้ง่ายๆ นะครับ และฟังก์ชัน read_file_content ก็จะทำการเปิดไฟล์ อ่าน แล้วก็แสดงผลออกมาให้เราเห็น

เพิ่ม Logging เข้าไป

เพื่อให้โปรแกรมของเราดูโปรขึ้น แล้วก็มีข้อมูลเวลาทำงาน เรามาเพิ่ม logging เข้าไปหน่อยนะครับ จะได้รู้ว่าเกิดอะไรขึ้นบ้าง

import argparse
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def read_file_content(filepath):
    try:
        logging.info(f"Attempting to read file: {filepath}")
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        logging.info(f"Successfully read file: {filepath}")
        # print(f"Content from {filepath}:\
{content}") # คอมเมนต์ออกไป ไม่ให้ interfere กับ test
        return content
    except FileNotFoundError:
        logging.error(f"Error: File not found at {filepath}")
        # print(f"Error: File not found at {filepath}")
        return None
    except Exception as e:
        logging.error(f"An unexpected error occurred: {e}")
        # print(f"An error occurred: {e}")
        return None

def main():
    parser = argparse.ArgumentParser(description="A simple file reader CLI tool.")
    parser.add_argument("filepath", type=str, help="Path to the file to read.")
    args = parser.parse_args()
    read_file_content(args.filepath)

if __name__ == "__main__":
    main()

แล้วก็ตรงนี้นะครับ เราเพิ่ม logging.basicConfig เข้ามาเพื่อให้ Logger ของเราทำงานได้ แล้วก็ใช้ logging.info กับ logging.error ในจุดที่เหมาะสมครับ

ลองสร้าง Context Manager เอง (Optional)

จริงๆ แล้ว open() ใน Python ก็เป็น context-manager อยู่แล้วนะครับ แต่ถ้าเพื่อนๆ อยากลองสร้างเอง เพื่อควบคุมทรัพยากรอื่นๆ ที่ไม่เกี่ยวกับไฟล์ ผมจะแสดงตัวอย่างง่ายๆ ให้ดูครับ

import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class ConnectionManager:
    def __init__(self, resource_name):
        self.resource_name = resource_name

    def __enter__(self):
        logging.info(f"Connecting to {self.resource_name}...")
        # สมมติว่านี่คือการเชื่อมต่อกับฐานข้อมูลหรือเครือข่าย
        return self.resource_name  # ส่งคืนตัว resource กลับไปใช้งาน

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            logging.error(f"Error with {self.resource_name}: {exc_val}")
        logging.info(f"Disconnecting from {self.resource_name}...")
        # ทำความสะอาด หรือปิดการเชื่อมต่อที่นี่
        return False # ถ้าเป็น True จะเป็นการ suppress error

# การใช้งาน
if __name__ == "__main__":
    with ConnectionManager("MyDatabase") as db_conn:
        logging.info(f"Using connection: {db_conn}")
        # จำลองการเกิดข้อผิดพลาด
        # raise ValueError("Something went wrong during DB operation")

    with ConnectionManager("MyAPI") as api_conn:
        logging.info(f"Working with {api_conn}")

ตรงนี้ครับ จะเห็นว่า context-manager ช่วยให้เรามั่นใจได้ว่า __exit__ จะถูกเรียกเสมอ ไม่ว่าจะเกิด Error หรือไม่ก็ตามนะครับ ทำให้การจัดการทรัพยากรของเราเป็นระเบียบและปลอดภัยครับ

การเขียน Test สำหรับ CLI ของเรา

แล้วก็ อย่าลืมเขียน Test ด้วยนะครับ เพื่อให้มั่นใจว่าโปรแกรมของเราทำงานได้ถูกต้องเสมอ เราสามารถใช้ unittest ใน Python ได้เลยครับ เราจะจำลองการเรียก CLI แล้วก็ตรวจสอบผลลัพธ์นะครับ

สร้างไฟล์ test_cli_tool.py ขึ้นมาแบบนี้นะครับ:

import unittest
import os
from unittest.mock import patch, MagicMock
from io import StringIO

# เพื่อความง่ายในการทดสอบ ผมจะเอา read_file_content และ main มาไว้ในนี้เลยนะครับ
import argparse
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def read_file_content(filepath):
    try:
        logging.info(f"Attempting to read file: {filepath}")
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        logging.info(f"Successfully read file: {filepath}")
        return content
    except FileNotFoundError:
        logging.error(f"Error: File not found at {filepath}")
        return None
    except Exception as e:
        logging.error(f"An unexpected error occurred: {e}")
        return None

def main():
    parser = argparse.ArgumentParser(description="A simple file reader CLI tool.")
    parser.add_argument("filepath", type=str, help="Path to the file to read.")
    args = parser.parse_args()
    read_file_content(args.filepath)


class TestCLITool(unittest.TestCase):

    def setUp(self):
        # สร้างไฟล์ทดสอบชั่วคราว
        self.test_filename = "temp_test_file.txt"
        with open(self.test_filename, "w") as f:
            f.write("Hello, World!\
This is a test.")

    def tearDown(self):
        # ลบไฟล์ทดสอบเมื่อจบการทดสอบ
        if os.path.exists(self.test_filename):
            os.remove(self.test_filename)

    def test_read_existing_file(self):
        content = read_file_content(self.test_filename)
        self.assertIsNotNone(content)
        self.assertIn("Hello, World!", content)

    def test_read_non_existent_file(self):
        content = read_file_content("non_existent_file.txt")
        self.assertIsNone(content)

    @patch('sys.argv', ['cli_tool.py', 'non_existent_file.txt'])
    @patch('sys.stdout', new_callable=StringIO)
    def test_main_with_non_existent_file_output(self, mock_stdout):
        # การรัน main() จะเรียก argparse ซึ่งจะ print error ไปที่ stdout
        # เราต้องจับ stdout เพื่อตรวจสอบข้อความ
        main() # main() จะเรียก read_file_content ซึ่งจะ print error ด้วย
        self.assertIn("Error: File not found", mock_stdout.getvalue())

เพื่อรัน Test นะครับ ให้เปิด Terminal แล้วไปที่ Folder ที่มีไฟล์ test_cli_tool.py แล้วพิมพ์ python -m unittest test_cli_tool.py แบบนี้นะครับ

แบบนี้นะครับ เพื่อนๆ ก็สามารถสร้างเครื่องมือ CLI ของตัวเองได้ง่ายๆ ด้วย Python แล้วก็ยังมั่นใจได้ด้วยว่าโค้ดของเราทำงานได้ถูกต้อง มีการจัดการทรัพยากรดี และมีการบันทึก Log เวลาทำงานด้วยครับ ลองเอาไปปรับใช้ดูได้เลยครับ

cii3.net

Read more

ดราม่า "เบิร์ด วันว่างๆ" กับยาแนว: บทเรียนสำคัญของอินฟลูเอนเซอร์และความปลอดภัยบนโซเชียล

ดราม่า "เบิร์ด วันว่างๆ" กับยาแนว: บทเรียนสำคัญของอินฟลูเอนเซอร์และความปลอดภัยบนโซเชียล

เจาะลึกดราม่า "เบิร์ด วันว่างๆ" ใช้ยาแนวเล่นสงกรานต์ คำชี้แจง และผลกระทบต่อสังคม บทเรียนสำคัญสำหรับความรับผิดชอบของอินฟลูเอนเซอร์

By ทีมงาน devdog
Xiaomi 17T หลุดสเปคเด็ดบน Geekbench! ยืนยัน Dimensity 8500 พร้อมแบต 7,000mAh จ่อเปิดตัว

Xiaomi 17T หลุดสเปคเด็ดบน Geekbench! ยืนยัน Dimensity 8500 พร้อมแบต 7,000mAh จ่อเปิดตัว

Xiaomi 17T เตรียมเปิดตัว! พบข้อมูลบน Geekbench ยืนยันใช้ชิป Dimensity 8500 พร้อมแบตเตอรี่จุใจ 7,000mAh คาดบุกตลาดรองเรือธงเร็วๆ นี้

By ทีมงาน devdog
กยศ. เปิดทางรอด! ปรับโครงสร้างหนี้ออนไลน์ หยุดถูกฟ้อง ก่อน 5 ก.ค. 69

กยศ. เปิดทางรอด! ปรับโครงสร้างหนี้ออนไลน์ หยุดถูกฟ้อง ก่อน 5 ก.ค. 69

ผู้กู้ กยศ. กว่า 1 แสนราย เสี่ยงถูกฟ้อง! รีบปรับโครงสร้างหนี้ออนไลน์ผ่านเป๋าตัง/ThaID ก่อน 5 ก.ค. 69 รับสิทธิประโยชน์ ลดดอกเบี้ย หลีกเลี่ยงคดีความ

By ทีมงาน devdog
Baseus MC2: หูฟังคลิปหนีบหูสุดล้ำ แบตอึด 60 ชม. เสียง LDAC กันน้ำ IP67 เพื่ออิสระทางเสียงของคุณ

Baseus MC2: หูฟังคลิปหนีบหูสุดล้ำ แบตอึด 60 ชม. เสียง LDAC กันน้ำ IP67 เพื่ออิสระทางเสียงของคุณ

พบ Baseus MC2 หูฟังคลิปหนีบหูดีไซน์ล้ำ สวมใส่สบายตลอดวัน ด้วยแบตเตอรี่ 60 ชม., กันน้ำ IP67, เสียง LDAC ระดับ Hi-Res และราคาเข้าถึงง่าย

By ทีมงาน devdog