Python作為當今最受歡迎的程式語言之一,其生態系統的豐富程度與套件管理機制的成熟度功不可沒。然而,對於許多開發者而言,將自己精心編寫的程式碼打包成可供他人安裝使用的套件,仍然是一個充滿挑戰與困惑的領域。Python的打包生態系統經歷了多年的演進,從早期的distutils到setuptools,再到如今的pyproject.toml標準,每一次變革都帶來了更好的開發體驗與更完善的功能支援。本文將從打包的基礎概念出發,深入探討現代Python專案打包的各個面向,包括專案結構設計、元資料配置、依賴管理、建構流程,以及發布到PyPI的完整策略,協助讀者掌握專業級的打包技能。

Python打包系統的核心目標在於解決程式碼分發與依賴管理的問題。當我們開發一個Python專案時,通常會依賴許多外部套件來實現各種功能。同樣地,我們自己的程式碼也可能被其他專案所依賴。打包機制提供了一種標準化的方式來描述套件的元資料、宣告其依賴關係,並定義安裝程序。透過這套機制,使用者只需執行簡單的pip install命令,就能自動下載套件及其所有依賴,並將它們正確安裝到Python環境中。理解這個系統的運作原理,不僅能幫助我們更好地發布自己的套件,也能讓我們在使用他人套件時更清楚其內部結構。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "Python打包生態系統" {
    rectangle "源碼管理" as source {
        card "pyproject.toml" as pyproject
        card "setup.cfg" as setupcfg
        card "setup.py" as setuppy
    }

    rectangle "建構工具" as build {
        card "setuptools" as setuptools
        card "build" as buildt
        card "flit" as flit
        card "poetry" as poetry
    }

    rectangle "發布格式" as dist {
        card "sdist (.tar.gz)" as sdist
        card "wheel (.whl)" as wheel
    }

    rectangle "套件倉庫" as repo {
        card "PyPI" as pypi
        card "TestPyPI" as testpypi
        card "私有倉庫" as private
    }

    rectangle "安裝工具" as install {
        card "pip" as pip
        card "pipx" as pipx
    }
}

source --> build
build --> dist
dist --> repo
repo --> install

@enduml

打包流程的第一步是理解Python專案的標準結構。一個設計良好的專案結構不僅能讓程式碼更易於維護,也能確保打包過程的順利進行。傳統上,Python專案有兩種主要的佈局方式,分別是flat layout與src layout。在flat layout中,套件目錄直接放置在專案根目錄下,這種方式簡單直觀,但存在一個潛在問題:當我們在專案目錄中執行Python時,直譯器會優先從當前目錄匯入模組,這意味著我們可能無意中測試的是未安裝的原始碼而非已安裝的套件。src layout則將套件目錄放置在src子目錄下,強制開發者必須先安裝套件才能匯入它,從而確保測試環境與生產環境的一致性。

# 專案結構範例:src佈局
# 這是現代Python專案推薦的目錄結構
# src佈局確保測試環境與生產環境的一致性

"""
my_project/                    # 專案根目錄
├── src/                       # 源碼目錄,包含所有可安裝的套件
│   └── my_package/           # 主套件目錄
│       ├── __init__.py       # 套件初始化檔案,定義公開介面
│       ├── core.py           # 核心功能模組
│       ├── utils.py          # 工具函式模組
│       └── cli.py            # 命令列介面模組
├── tests/                     # 測試目錄
│   ├── __init__.py           # 測試套件初始化
│   ├── test_core.py          # 核心功能測試
│   └── test_utils.py         # 工具函式測試
├── docs/                      # 文件目錄
│   ├── conf.py               # Sphinx配置
│   └── index.rst             # 文件首頁
├── pyproject.toml            # 專案配置與建構系統定義
├── README.md                 # 專案說明文件
├── LICENSE                   # 授權條款檔案
└── .gitignore                # Git忽略規則
"""

# __init__.py範例:定義套件的公開介面與版本資訊
# 此檔案在套件被匯入時自動執行

# 定義套件版本號,遵循語義化版本規範
# 主版本號.次版本號.修訂號
__version__ = "1.2.3"

# 定義套件作者資訊
__author__ = "開發團隊"

# 從子模組匯入核心類別,簡化使用者的匯入路徑
# 使用者可以直接 from my_package import DataProcessor
from .core import DataProcessor
from .core import ConfigManager

# 從工具模組匯入常用函式
from .utils import format_output
from .utils import validate_input

# 定義 __all__ 變數,明確指定公開的名稱
# 當使用者執行 from my_package import * 時
# 只會匯入這個清單中的項目
__all__ = [
    "DataProcessor",      # 資料處理核心類別
    "ConfigManager",      # 配置管理類別
    "format_output",      # 輸出格式化函式
    "validate_input",     # 輸入驗證函式
    "__version__",        # 版本號
]

現代Python專案應當使用pyproject.toml作為配置檔案,這是PEP 517、PEP 518與PEP 621所定義的標準格式。pyproject.toml採用TOML語法,具有良好的可讀性與結構化特性。這個檔案不僅用於定義專案的元資料,還指定了建構系統的後端工具。在pyproject.toml出現之前,Python專案需要維護多個配置檔案,包括setup.py、setup.cfg、MANIFEST.in等,這些檔案各自負責不同的配置項目,容易造成混淆與不一致。pyproject.toml的設計目標是將所有配置集中到單一檔案中,提供更清晰、更一致的開發體驗。

# pyproject.toml 完整配置範例
# 這是現代Python專案的核心配置檔案
# 使用TOML格式,具有良好的可讀性

# 建構系統配置區段
# 定義用於建構套件的後端工具
[build-system]
# 列出建構時需要的依賴套件
# setuptools是最常用的建構後端
requires = [
    "setuptools>=61.0",    # setuptools 61.0版本開始完整支援pyproject.toml
    "wheel"                # wheel套件用於建構wheel格式的發布檔
]
# 指定建構後端的模組路徑
# setuptools.build_meta是setuptools提供的PEP 517相容介面
build-backend = "setuptools.build_meta"

# 專案元資料區段
# 定義套件的基本資訊,遵循PEP 621規範
[project]
# 套件名稱,在PyPI上必須唯一
# 建議使用小寫字母和底線,避免使用連字號
name = "my-awesome-package"

# 動態欄位清單
# 這些欄位的值將在建構時由建構後端自動決定
dynamic = ["version"]

# 套件的簡短描述,顯示在PyPI的搜尋結果中
description = "一個功能強大的Python工具套件,提供資料處理與分析功能"

# 詳細說明文件
# 通常讀取README.md的內容作為PyPI頁面的主要說明
readme = "README.md"

# 要求的最低Python版本
# 使用比較運算子指定版本範圍
requires-python = ">=3.8"

# 授權條款
license = {text = "MIT"}

# 作者資訊清單
# 可以包含多位作者
authors = [
    {name = "張開發", email = "developer@example.com"},
    {name = "李維護", email = "maintainer@example.com"}
]

# 維護者資訊,與作者分開列出
maintainers = [
    {name = "王貢獻", email = "contributor@example.com"}
]

# 關鍵字清單,協助使用者在PyPI上搜尋
keywords = [
    "data-processing",
    "analysis",
    "automation",
    "utility"
]

# 分類器清單,用於PyPI的分類系統
# 必須使用PyPI官方定義的分類器字串
classifiers = [
    # 開發狀態
    "Development Status :: 4 - Beta",
    # 目標使用者
    "Intended Audience :: Developers",
    # 授權條款
    "License :: OSI Approved :: MIT License",
    # 程式語言版本
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    # 作業系統
    "Operating System :: OS Independent",
    # 主題分類
    "Topic :: Software Development :: Libraries :: Python Modules"
]

# 執行時依賴清單
# 這些套件會在安裝本套件時自動安裝
dependencies = [
    "requests>=2.25.0",        # HTTP請求庫,指定最低版本
    "pyyaml>=5.4",             # YAML解析庫
    "click>=8.0",              # 命令列介面框架
    "rich>=10.0",              # 終端機美化輸出
]

# 專案相關連結
[project.urls]
"Homepage" = "https://github.com/username/my-awesome-package"
"Bug Tracker" = "https://github.com/username/my-awesome-package/issues"
"Documentation" = "https://my-awesome-package.readthedocs.io"
"Source Code" = "https://github.com/username/my-awesome-package"
"Changelog" = "https://github.com/username/my-awesome-package/blob/main/CHANGELOG.md"

# 可選依賴群組
# 使用者可以透過 pip install my-package[dev] 安裝特定群組
[project.optional-dependencies]
# 開發環境依賴
dev = [
    "pytest>=7.0",             # 測試框架
    "pytest-cov>=3.0",         # 測試覆蓋率報告
    "black>=22.0",             # 程式碼格式化工具
    "isort>=5.10",             # import排序工具
    "mypy>=0.950",             # 靜態型別檢查
    "ruff>=0.0.260",           # 快速程式碼檢查工具
]
# 文件建置依賴
docs = [
    "sphinx>=4.5",             # 文件生成工具
    "sphinx-rtd-theme>=1.0",   # ReadTheDocs主題
    "myst-parser>=0.18",       # Markdown支援
]
# 測試依賴
test = [
    "pytest>=7.0",
    "pytest-cov>=3.0",
    "pytest-mock>=3.7",        # Mock功能支援
    "responses>=0.20",         # HTTP回應模擬
]

# 命令列程式進入點
# 安裝後可以直接在終端機執行這些命令
[project.scripts]
# 命令名稱 = "模組路徑:函式名稱"
my-tool = "my_package.cli:main"
my-tool-gui = "my_package.gui:launch"

# GUI程式進入點
[project.gui-scripts]
my-app = "my_package.app:run"

# setuptools特定配置
[tool.setuptools]
# 指定套件目錄位置
package-dir = {"" = "src"}

# 動態版本配置
# 從套件的__init__.py讀取__version__變數
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}

# 自動探索套件
[tool.setuptools.packages.find]
where = ["src"]  # 在src目錄下搜尋套件

依賴管理是打包過程中最需要謹慎處理的環節之一。Python套件的依賴分為多種類型,包括執行時依賴、建構時依賴、測試依賴與開發依賴。執行時依賴是套件正常運作所必需的,必須宣告在dependencies欄位中。建構時依賴用於套件的建構過程,宣告在build-system.requires中。測試依賴與開發依賴則通常放在optional-dependencies中,讓使用者可以選擇性安裝。在宣告依賴時,應當明確指定版本範圍。過於寬鬆的版本範圍可能導致相容性問題,而過於嚴格的版本範圍則可能造成依賴衝突。一般建議使用>=指定最低版本,並在確有必要時使用<限制最高版本。

# 依賴管理最佳實踐範例
# 展示如何在程式碼中正確處理依賴相關的問題

# 標準庫匯入區段
# 這些模組不需要額外安裝
import sys
import os
import json
from typing import Optional, List, Dict, Any
from pathlib import Path

# 檢查Python版本的函式
# 確保執行環境符合套件的最低需求
def check_python_version(
    minimum_version: tuple = (3, 8)  # 最低版本要求
) -> bool:
    """
    檢查當前Python版本是否符合最低需求。

    此函式用於在套件匯入時驗證執行環境。
    如果版本不符,應當提供清楚的錯誤訊息。

    參數:
        minimum_version: 最低版本號元組,預設為(3, 8)

    回傳:
        bool: 版本符合需求時回傳True

    異常:
        RuntimeError: 當Python版本低於最低需求時拋出
    """
    # 取得當前Python版本資訊
    current_version = sys.version_info[:2]

    # 比較版本號
    if current_version < minimum_version:
        # 格式化版本號字串
        current_str = f"{current_version[0]}.{current_version[1]}"
        minimum_str = f"{minimum_version[0]}.{minimum_version[1]}"

        # 拋出包含詳細資訊的異常
        raise RuntimeError(
            f"此套件需要Python {minimum_str}或更高版本,"
            f"當前版本為{current_str}。"
            f"請升級您的Python版本。"
        )

    return True

# 條件匯入範例
# 處理可選依賴的正確方式
def import_optional_dependency(
    module_name: str,           # 模組名稱
    package_name: str = None,   # pip套件名稱(如果與模組名不同)
    minimum_version: str = None # 最低版本要求
) -> Any:
    """
    嘗試匯入可選依賴模組。

    此函式提供了優雅處理可選依賴的方式。
    當依賴不可用時,不會直接拋出ImportError,
    而是回傳None並提供安裝指引。

    參數:
        module_name: 要匯入的模組名稱
        package_name: pip安裝時使用的套件名稱
        minimum_version: 最低版本要求字串

    回傳:
        匯入的模組物件,或None如果匯入失敗
    """
    # 如果未指定套件名稱,使用模組名稱
    if package_name is None:
        package_name = module_name

    try:
        # 使用importlib動態匯入模組
        import importlib
        module = importlib.import_module(module_name)

        # 檢查版本(如果有要求)
        if minimum_version is not None:
            # 嘗試取得模組版本
            module_version = getattr(module, "__version__", None)

            if module_version is None:
                # 某些模組使用不同的版本屬性名稱
                module_version = getattr(module, "VERSION", None)

            if module_version is not None:
                # 使用packaging庫進行版本比較
                from packaging.version import Version

                if Version(module_version) < Version(minimum_version):
                    print(
                        f"警告: {module_name} 版本 {module_version} "
                        f"低於建議的 {minimum_version}。"
                        f"建議執行: pip install --upgrade {package_name}"
                    )

        return module

    except ImportError as e:
        # 提供詳細的錯誤訊息和安裝指引
        print(
            f"無法匯入 {module_name}。"
            f"此為可選依賴,某些功能將無法使用。"
            f"如需使用相關功能,請執行: pip install {package_name}"
        )
        return None

# 延遲匯入模式
# 只在實際需要時才匯入依賴
class LazyLoader:
    """
    延遲載入器類別。

    此類別實現了延遲匯入模式,模組只有在首次存取時
    才會被實際匯入。這對於有大量可選依賴的套件特別有用,
    可以顯著縮短套件的初始匯入時間。
    """

    def __init__(
        self,
        module_name: str,      # 模組名稱
        package_name: str = None  # pip套件名稱
    ):
        """
        初始化延遲載入器。

        參數:
            module_name: 要延遲載入的模組名稱
            package_name: pip安裝時使用的套件名稱
        """
        # 儲存模組資訊
        self._module_name = module_name
        self._package_name = package_name or module_name
        # 快取載入的模組
        self._module = None

    def __getattr__(self, name: str) -> Any:
        """
        攔截屬性存取,觸發模組載入。

        當首次存取載入器的任何屬性時,
        會先載入目標模組,然後回傳該屬性。

        參數:
            name: 要存取的屬性名稱

        回傳:
            目標模組的對應屬性
        """
        # 載入模組(如果尚未載入)
        if self._module is None:
            import importlib
            try:
                self._module = importlib.import_module(self._module_name)
            except ImportError:
                raise ImportError(
                    f"需要 {self._module_name} 才能使用此功能。"
                    f"請執行: pip install {self._package_name}"
                )

        # 回傳模組的屬性
        return getattr(self._module, name)

# 使用延遲載入器
# 這些模組只有在被存取時才會實際匯入
pandas = LazyLoader("pandas")
numpy = LazyLoader("numpy")
matplotlib = LazyLoader("matplotlib.pyplot", "matplotlib")

版本管理是套件開發中的重要議題。良好的版本策略能夠清楚傳達套件的變更程度,協助使用者評估升級的風險。Python生態系統普遍採用語義化版本規範,版本號格式為MAJOR.MINOR.PATCH。MAJOR版本號在引入不相容的API變更時遞增,MINOR版本號在新增向後相容的功能時遞增,PATCH版本號在進行向後相容的錯誤修正時遞增。版本號應當定義在單一位置,避免多處重複定義造成的不一致問題。pyproject.toml支援從Python程式碼中動態讀取版本號,這是目前最推薦的做法。

# 版本管理進階範例
# 展示不同的版本管理策略與工具整合

# 使用單一來源的版本號管理
# __init__.py中定義,pyproject.toml動態讀取

# 版本號定義
# 遵循語義化版本規範 (SemVer)
__version__ = "2.1.0"

# 版本資訊元組,方便程式化比較
VERSION_INFO = (2, 1, 0)

# 詳細的版本資訊字典
VERSION_DETAILS = {
    "major": 2,           # 主版本號:不相容的API變更
    "minor": 1,           # 次版本號:新增向後相容功能
    "patch": 0,           # 修訂號:向後相容的錯誤修正
    "release": "stable",  # 發布類型:alpha/beta/rc/stable
    "build": None,        # 建構編號(如果有)
}

def get_version_string(include_build: bool = False) -> str:
    """
    取得格式化的版本字串。

    此函式產生標準化的版本字串,
    可選擇是否包含建構編號資訊。

    參數:
        include_build: 是否包含建構編號

    回傳:
        格式化的版本字串
    """
    # 基本版本字串
    version = __version__

    # 如果需要且有建構編號,則附加
    if include_build and VERSION_DETAILS["build"]:
        version = f"{version}+{VERSION_DETAILS['build']}"

    return version

def check_version_compatibility(
    required_version: str,       # 要求的版本
    comparison: str = ">="       # 比較運算子
) -> bool:
    """
    檢查當前版本是否符合要求。

    此函式用於在執行時驗證版本相容性,
    特別適用於外掛系統或API版本檢查。

    參數:
        required_version: 要求的版本字串
        comparison: 比較運算子,如>=、>、==、<、<=

    回傳:
        bool: 版本符合要求時回傳True
    """
    # 匯入版本比較工具
    from packaging.version import Version
    from packaging.specifiers import SpecifierSet

    # 建立版本規格
    specifier = SpecifierSet(f"{comparison}{required_version}")

    # 檢查當前版本是否符合規格
    return Version(__version__) in specifier

# 版本變更日誌管理
# 展示如何程式化管理CHANGELOG

class ChangelogEntry:
    """
    變更日誌條目類別。

    此類別用於結構化儲存版本變更資訊,
    可用於自動生成CHANGELOG文件。
    """

    def __init__(
        self,
        version: str,              # 版本號
        date: str,                 # 發布日期
        changes: Dict[str, List[str]]  # 變更內容
    ):
        """
        初始化變更日誌條目。

        參數:
            version: 版本號字串
            date: 發布日期,格式為YYYY-MM-DD
            changes: 變更內容字典,鍵為變更類型
        """
        self.version = version
        self.date = date
        self.changes = changes

    def to_markdown(self) -> str:
        """
        將變更條目轉換為Markdown格式。

        回傳:
            Markdown格式的變更記錄字串
        """
        # 標題行
        lines = [f"## [{self.version}] - {self.date}", ""]

        # 變更類型的順序
        type_order = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]

        # 依序處理每種變更類型
        for change_type in type_order:
            if change_type in self.changes:
                lines.append(f"### {change_type}")
                lines.append("")

                # 列出該類型的所有變更
                for change in self.changes[change_type]:
                    lines.append(f"- {change}")

                lines.append("")

        return "\n".join(lines)

# 範例變更日誌條目
CHANGELOG = [
    ChangelogEntry(
        version="2.1.0",
        date="2025-11-25",
        changes={
            "Added": [
                "新增資料匯出功能,支援CSV、JSON、Excel格式",
                "新增命令列介面,支援批次處理",
            ],
            "Changed": [
                "改善資料處理效能,速度提升30%",
                "更新相依套件至最新版本",
            ],
            "Fixed": [
                "修正Unicode編碼問題",
                "修正記憶體洩漏問題",
            ],
        }
    ),
    ChangelogEntry(
        version="2.0.0",
        date="2025-10-01",
        changes={
            "Changed": [
                "重構核心API,提供更一致的介面",
                "最低Python版本要求提升至3.8",
            ],
            "Removed": [
                "移除已棄用的legacy_function",
            ],
        }
    ),
]

建構與發布流程是將原始碼轉換為可分發格式的關鍵步驟。Python目前有兩種主要的發布格式,分別是source distribution與wheel。Source distribution是原始碼的壓縮檔,通常是tar.gz格式,包含了建構套件所需的所有檔案。Wheel則是預先建構好的二進位格式,使用者可以直接安裝而無需執行任何建構程式碼。Wheel格式具有多項優勢,包括安裝速度更快、更安全(不需要執行任意程式碼)、以及更好的可重現性。建議同時發布這兩種格式,source distribution確保任何平台都能安裝,而wheel則為常見平台提供最佳化的安裝體驗。

# 建構流程自動化範例
# 展示使用Python腳本自動化建構與發布流程

import subprocess
import shutil
from pathlib import Path
from typing import List, Optional

class PackageBuilder:
    """
    套件建構器類別。

    此類別封裝了Python套件的建構與發布流程,
    提供一致且可重複的建構體驗。
    """

    def __init__(
        self,
        project_root: Path = None  # 專案根目錄
    ):
        """
        初始化建構器。

        參數:
            project_root: 專案根目錄路徑,預設為當前目錄
        """
        # 設定專案根目錄
        self.project_root = project_root or Path.cwd()

        # 建構輸出目錄
        self.dist_dir = self.project_root / "dist"

        # 建構中間產物目錄
        self.build_dir = self.project_root / "build"

        # egg-info目錄
        self.egg_info_pattern = "*.egg-info"

    def clean(self) -> None:
        """
        清理先前的建構產物。

        此方法刪除所有先前建構產生的檔案和目錄,
        確保每次建構都從乾淨的狀態開始。
        """
        print("正在清理建構目錄...")

        # 清理dist目錄
        if self.dist_dir.exists():
            shutil.rmtree(self.dist_dir)
            print(f"  已刪除: {self.dist_dir}")

        # 清理build目錄
        if self.build_dir.exists():
            shutil.rmtree(self.build_dir)
            print(f"  已刪除: {self.build_dir}")

        # 清理egg-info目錄
        for egg_info in self.project_root.glob(f"**/{self.egg_info_pattern}"):
            # 跳過虛擬環境中的檔案
            if ".venv" not in str(egg_info) and "venv" not in str(egg_info):
                shutil.rmtree(egg_info)
                print(f"  已刪除: {egg_info}")

        # 清理__pycache__目錄
        for pycache in self.project_root.glob("**/__pycache__"):
            if ".venv" not in str(pycache) and "venv" not in str(pycache):
                shutil.rmtree(pycache)

        print("清理完成")

    def build(
        self,
        formats: List[str] = None  # 要建構的格式
    ) -> List[Path]:
        """
        建構套件發布檔。

        使用build模組建構套件,產生sdist與wheel格式。

        參數:
            formats: 要建構的格式清單,預設為['sdist', 'wheel']

        回傳:
            建構產生的檔案路徑清單
        """
        # 預設建構格式
        if formats is None:
            formats = ["sdist", "wheel"]

        print(f"正在建構套件(格式: {', '.join(formats)})...")

        # 建立輸出目錄
        self.dist_dir.mkdir(exist_ok=True)

        # 建構每種格式
        for fmt in formats:
            print(f"  建構 {fmt}...")

            # 使用python -m build命令
            # 這是目前推薦的建構方式
            cmd = [
                "python", "-m", "build",
                "--outdir", str(self.dist_dir),
                f"--{fmt}",
                str(self.project_root)
            ]

            # 執行建構命令
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True
            )

            # 檢查結果
            if result.returncode != 0:
                print(f"    錯誤: {result.stderr}")
                raise RuntimeError(f"建構{fmt}失敗")

            print(f"    成功")

        # 列出建構產物
        built_files = list(self.dist_dir.glob("*"))
        print(f"建構完成,產生了 {len(built_files)} 個檔案:")

        for file in built_files:
            # 顯示檔案大小
            size = file.stat().st_size
            if size > 1024 * 1024:
                size_str = f"{size / 1024 / 1024:.1f} MB"
            elif size > 1024:
                size_str = f"{size / 1024:.1f} KB"
            else:
                size_str = f"{size} bytes"

            print(f"  {file.name} ({size_str})")

        return built_files

    def check(self) -> bool:
        """
        檢查建構產物的品質。

        使用twine check命令驗證發布檔是否符合PyPI的要求。

        回傳:
            bool: 檢查通過時回傳True
        """
        print("正在檢查發布檔品質...")

        # 取得所有發布檔
        dist_files = list(self.dist_dir.glob("*"))

        if not dist_files:
            print("  錯誤: 找不到發布檔,請先執行build")
            return False

        # 使用twine check命令
        cmd = [
            "python", "-m", "twine", "check",
            *[str(f) for f in dist_files]
        ]

        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True
        )

        # 顯示結果
        if result.returncode == 0:
            print("  檢查通過")
            return True
        else:
            print(f"  檢查失敗: {result.stdout}")
            return False

    def upload(
        self,
        repository: str = "pypi",  # 目標倉庫
        skip_existing: bool = True   # 是否跳過已存在的版本
    ) -> bool:
        """
        上傳發布檔到套件倉庫。

        使用twine upload命令將建構好的發布檔上傳到PyPI或其他倉庫。

        參數:
            repository: 目標倉庫,'pypi'或'testpypi'
            skip_existing: 是否跳過已存在的版本

        回傳:
            bool: 上傳成功時回傳True
        """
        print(f"正在上傳到 {repository}...")

        # 取得所有發布檔
        dist_files = list(self.dist_dir.glob("*"))

        if not dist_files:
            print("  錯誤: 找不到發布檔,請先執行build")
            return False

        # 建構twine命令
        cmd = [
            "python", "-m", "twine", "upload"
        ]

        # 設定倉庫
        if repository == "testpypi":
            cmd.extend(["--repository", "testpypi"])

        # 跳過已存在的版本
        if skip_existing:
            cmd.append("--skip-existing")

        # 新增發布檔
        cmd.extend([str(f) for f in dist_files])

        # 執行上傳
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            print("  上傳成功")
            return True
        else:
            print(f"  上傳失敗: {result.stderr}")
            return False

# 建構腳本使用範例
if __name__ == "__main__":
    # 建立建構器實例
    builder = PackageBuilder()

    # 執行完整的建構流程
    # 1. 清理先前的建構產物
    builder.clean()

    # 2. 建構sdist和wheel
    builder.build()

    # 3. 檢查發布檔品質
    if builder.check():
        # 4. 上傳到TestPyPI進行測試
        # builder.upload(repository="testpypi")

        # 5. 測試通過後上傳到正式PyPI
        # builder.upload(repository="pypi")
        pass

測試是確保套件品質的關鍵環節。在打包之前,應當確保所有測試都能在已安裝的套件上通過,而非直接在原始碼上測試。這是使用src layout的主要優勢之一,它強制開發者必須先安裝套件才能執行測試。pytest是Python生態系統中最受歡迎的測試框架,它提供了豐富的功能與良好的擴充性。在pyproject.toml中可以配置pytest的選項,包括測試目錄、命令列參數、以及各種外掛的設定。除了單元測試之外,也應當考慮加入整合測試、效能測試、以及跨平台測試,確保套件在各種環境下都能正常運作。

# 測試配置與範例
# 展示如何為打包的套件撰寫測試

import pytest
from pathlib import Path
from typing import Generator, Any

# 假設我們要測試的是my_package套件
# 這裡展示測試的結構與最佳實踐

# Fixture範例:提供測試所需的資源
@pytest.fixture
def sample_data_dir(tmp_path: Path) -> Path:
    """
    提供包含測試資料的臨時目錄。

    此fixture使用pytest的tmp_path fixture建立臨時目錄,
    並在其中建立測試所需的檔案。測試結束後會自動清理。

    參數:
        tmp_path: pytest提供的臨時目錄fixture

    回傳:
        包含測試資料的目錄路徑
    """
    # 建立測試資料目錄
    data_dir = tmp_path / "test_data"
    data_dir.mkdir()

    # 建立測試用的JSON檔案
    json_file = data_dir / "config.json"
    json_file.write_text(
        '{"name": "test", "value": 42}',
        encoding="utf-8"
    )

    # 建立測試用的文字檔案
    text_file = data_dir / "data.txt"
    text_file.write_text(
        "line 1\nline 2\nline 3",
        encoding="utf-8"
    )

    return data_dir

@pytest.fixture
def mock_api_response() -> dict:
    """
    提供模擬的API回應資料。

    此fixture用於測試需要API回應的功能,
    避免在測試中進行實際的網路請求。

    回傳:
        模擬的API回應字典
    """
    return {
        "status": "success",
        "data": {
            "id": 12345,
            "items": ["a", "b", "c"],
            "metadata": {
                "count": 3,
                "timestamp": "2025-11-25T10:00:00Z"
            }
        }
    }

# 測試類別範例
class TestDataProcessor:
    """
    DataProcessor類別的測試集。

    此測試類別包含了DataProcessor各項功能的單元測試,
    確保核心功能在各種情況下都能正確運作。
    """

    def test_initialization(self):
        """
        測試DataProcessor的初始化。

        確認物件能夠正確建立,且具有預期的初始狀態。
        """
        # 匯入要測試的類別
        from my_package import DataProcessor

        # 建立實例
        processor = DataProcessor()

        # 驗證初始狀態
        assert processor is not None
        assert processor.data == []
        assert processor.is_initialized is True

    def test_load_from_file(
        self,
        sample_data_dir: Path  # 使用fixture提供的測試資料
    ):
        """
        測試從檔案載入資料的功能。

        此測試驗證DataProcessor能夠正確讀取和解析檔案內容。

        參數:
            sample_data_dir: fixture提供的測試資料目錄
        """
        from my_package import DataProcessor

        # 建立實例
        processor = DataProcessor()

        # 載入測試檔案
        json_path = sample_data_dir / "config.json"
        result = processor.load_from_file(json_path)

        # 驗證載入結果
        assert result is True
        assert processor.data is not None
        assert processor.data["name"] == "test"
        assert processor.data["value"] == 42

    def test_load_nonexistent_file(self):
        """
        測試載入不存在的檔案時的錯誤處理。

        確認當檔案不存在時,會拋出適當的異常。
        """
        from my_package import DataProcessor

        processor = DataProcessor()

        # 使用pytest.raises檢查異常
        with pytest.raises(FileNotFoundError) as exc_info:
            processor.load_from_file(Path("/nonexistent/file.json"))

        # 驗證異常訊息
        assert "找不到檔案" in str(exc_info.value)

    @pytest.mark.parametrize(
        "input_value,expected",
        [
            ("hello", "HELLO"),           # 一般字串
            ("Hello World", "HELLO WORLD"),  # 包含空格
            ("123", "123"),               # 數字字串
            ("", ""),                     # 空字串
            ("café", "CAFÉ"),             # 包含重音字母
        ]
    )
    def test_transform_uppercase(
        self,
        input_value: str,
        expected: str
    ):
        """
        測試轉換為大寫的功能。

        使用參數化測試驗證各種輸入情況。

        參數:
            input_value: 輸入字串
            expected: 預期輸出
        """
        from my_package import DataProcessor

        processor = DataProcessor()
        result = processor.transform(input_value, "uppercase")

        assert result == expected

    def test_api_integration(
        self,
        mock_api_response: dict,  # 使用fixture提供的模擬回應
        monkeypatch                # pytest提供的monkeypatch fixture
    ):
        """
        測試API整合功能。

        使用monkeypatch替換實際的API呼叫,
        確保測試不依賴外部服務。

        參數:
            mock_api_response: fixture提供的模擬回應
            monkeypatch: pytest的monkeypatch fixture
        """
        from my_package import DataProcessor
        import my_package.core as core_module

        # 定義模擬函式
        def mock_fetch_data(url):
            return mock_api_response

        # 替換實際的API呼叫
        monkeypatch.setattr(
            core_module,
            "fetch_data",
            mock_fetch_data
        )

        # 執行測試
        processor = DataProcessor()
        result = processor.fetch_and_process("https://api.example.com/data")

        # 驗證結果
        assert result["status"] == "success"
        assert len(result["data"]["items"]) == 3

# 效能測試範例
class TestPerformance:
    """
    效能測試集。

    此測試類別包含效能相關的測試,
    確保套件在處理大量資料時維持合理的效能。
    """

    @pytest.mark.slow  # 標記為慢速測試,可選擇性跳過
    def test_large_dataset_processing(self):
        """
        測試處理大型資料集的效能。

        此測試驗證處理器能夠在合理時間內處理大量資料。
        """
        from my_package import DataProcessor
        import time

        # 建立大型測試資料
        large_data = [{"id": i, "value": i * 2} for i in range(10000)]

        processor = DataProcessor()
        processor.data = large_data

        # 測量處理時間
        start_time = time.time()
        result = processor.process_all()
        elapsed_time = time.time() - start_time

        # 驗證效能(應在1秒內完成)
        assert elapsed_time < 1.0, f"處理時間過長: {elapsed_time:.2f}秒"
        assert len(result) == 10000

發布到PyPI是分享套件的最後一步。PyPI是Python官方的套件索引,任何人都可以在上面搜尋和安裝套件。在正式發布之前,建議先使用TestPyPI進行測試,確保套件能夠正確上傳和安裝。上傳套件需要使用twine工具,它提供了安全的上傳機制,支援雙因素驗證。發布後應當持續維護套件,包括修復錯誤、回應使用者問題、以及更新文件。良好的維護習慣能夠建立使用者的信任,提升套件的採用率。

Python打包生態系統仍在持續演進中。近年來,poetry和flit等新工具的出現提供了更簡化的打包體驗,而pyproject.toml標準的確立則為各種工具提供了統一的配置介面。然而,setuptools作為最成熟的建構後端,仍然是大多數專案的首選。無論選擇哪種工具,理解打包的基本概念與最佳實踐都是必要的。本文介紹的原則與模式適用於各種打包工具,讀者可以根據專案的特性選擇最適合的方案。

除了技術層面的考量之外,打包也涉及許多軟性因素。清晰的文件是套件成功的關鍵,包括API參考、使用指南、以及範例程式碼。良好的錯誤訊息能夠幫助使用者快速定位問題。完整的測試覆蓋率則確保套件的穩定性。這些因素雖然不直接影響打包流程,但對套件的整體品質有著決定性的影響。

總結而言,Python專案打包是一個系統性的工程,需要考量專案結構、配置管理、依賴處理、版本策略、建構流程、測試驗證、以及發布維護等多個面向。透過遵循本文介紹的最佳實踐,開發者可以建立專業級的Python套件,為使用者提供優質的安裝與使用體驗。隨著Python生態系統的持續發展,打包工具與標準也會不斷改進,保持學習與更新的習慣是維持競爭力的關鍵。