สร้าง 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

ไอลีน กู: ตำนานนักสกีฟรีสไตล์ผู้พลิกโฉมวงการและความหมายของชัยชนะ

ไอลีน กู: ตำนานนักสกีฟรีสไตล์ผู้พลิกโฉมวงการและความหมายของชัยชนะ

เจาะลึกเรื่องราวของ Eileen Gu นักสกีฟรีสไตล์ผู้สร้างประวัติศาสตร์ในโอลิมปิก 2026 สถิติที่ไม่เคยมีมาก่อน ประเด็นถกเถียง และความแข็งแกร่งส่วนตัวที่ทำให้เธอก้าวสู่ระดับโลก

By ทีมงาน devdog
วันพระ: คู่มือฉบับสมบูรณ์สำหรับพุทธศาสนิกชนและผู้สนใจยุคใหม่

วันพระ: คู่มือฉบับสมบูรณ์สำหรับพุทธศาสนิกชนและผู้สนใจยุคใหม่

เจาะลึกวันพระและความสำคัญของวันมาฆบูชา 2569 ทั้งวันหยุดราชการ ธนาคาร กิจกรรมเวียนเทียนต้นไม้ และผลกระทบต่อบริการขนส่ง เตรียมตัววางแผนทำบุญและพักผ่อน

By ทีมงาน devdog
ถอดรหัสรักแท้: "บังมัดคลองตันต้นข้าว" เรื่องราวที่สะท้อนการให้อภัยและการเริ่มต้นใหม่

ถอดรหัสรักแท้: "บังมัดคลองตันต้นข้าว" เรื่องราวที่สะท้อนการให้อภัยและการเริ่มต้นใหม่

เจาะลึกงานวิวาห์ "บังมัดคลองตัน" กับ "ต้นข้าว มิสแกรนด์" พร้อมเหตุผลจากใจเจ้าสาวที่เลือกความรักเหนือกาลเวลาและคำวิจารณ์ สู่การเริ่มต้นชีวิตคู่ที่สะท้อนการให้อภัย

By ทีมงาน devdog
ไฮไลท์บอลไทยลีก 2: มหาสารคาม เอสบีที เอฟซี กับฟอร์มร้อนแรงสู่เส้นทางเพลย์ออฟ

ไฮไลท์บอลไทยลีก 2: มหาสารคาม เอสบีที เอฟซี กับฟอร์มร้อนแรงสู่เส้นทางเพลย์ออฟ

เจาะลึกไฮไลท์บอลไทยลีก 2 ของมหาสารคาม เอสบีที เอฟซี กับฟอร์มร้อนแรง ชัยชนะสำคัญจาก ชิตชนก และบทบาทโค้ชดุสิต สู่เส้นทางเพลย์ออฟที่น่าจับตา!

By ทีมงาน devdog