Phần I: Nền tảng của một Python Package
Việc xây dựng một package Python chuyên nghiệp không chỉ bắt đầu từ việc viết mã nguồn mà còn từ việc thiết lập một nền tảng vững chắc về cấu trúc và tổ chức. Một nền tảng được thiết kế tốt sẽ đảm bảo tính dễ bảo trì, khả năng mở rộng và sự tương thích với các công cụ hiện đại trong hệ sinh thái Python. Phần này sẽ đi sâu vào các nguyên tắc nền tảng, từ cách bố trí vật lý của các tệp tin đến việc tổ chức logic của mã nguồn.
1.1. Cấu trúc Thư mục Hiện đại: src-layout và Các Quy ước
Cấu trúc thư mục của một dự án là bộ khung quyết định sự rõ ràng và hiệu quả trong quản lý mã nguồn. Đối với các dự án nhỏ, một cấu trúc phẳng có thể là đủ, nhưng khi dự án phát triển về quy mô và độ phức tạp, việc áp dụng một cấu trúc thư mục có thứ bậc, theo module là cực kỳ quan trọng.
Tiêu chuẩn hiện đại và được khuyến nghị rộng rãi trong cộng đồng Python là cấu trúc src-layout
. Cấu trúc này tạo ra một sự tách biệt rõ ràng giữa mã nguồn có thể cài đặt của package và các thành phần khác của dự án như tệp kiểm thử (tests), tài liệu (documentation), và các tệp cấu hình. Một cấu trúc src-layout
điển hình trông như sau:
project_name/
├── src/
│ └── package_name/
│ ├── __init__.py
│ └── module1.py
├── tests/
│ ├── __init__.py
│ └── test_module1.py
├── docs/
├── pyproject.toml
├── README.md
└── LICENSE
Trong đó:
src/
: Thư mục chứa mã nguồn chính của package. Việc đặt mã nguồn vào đây là điểm đặc trưng củasrc-layout
.src/package_name/
: Đây chính là package Python sẽ được cài đặt.tests/
: Thư mục chứa tất cả các tệp kiểm thử tự động, tách biệt hoàn toàn khỏi mã nguồn sản phẩm.docs/
: Thư mục chứa các tệp nguồn để xây dựng tài liệu cho dự án.pyproject.toml
,README.md
,LICENSE
: Các tệp cấu hình, mô tả và giấy phép ở cấp cao nhất của dự án.
Việc tuân thủ các quy ước đặt tên theo PEP 8 là yếu tố không thể thiếu để đảm bảo tính nhất quán và dễ đọc. Tên module và package nên sử dụng chữ thường và dấu gạch dưới (ví dụ: my_module.py
), trong khi tên lớp nên sử dụng quy ước CamelCase (ví dụ: MyClass
). Cần tránh các tên gọi chung chung như utils.py
; thay vào đó, hãy sử dụng tên cụ thể hơn để mô tả chức năng, ví dụ: string_utils.py
.
Việc áp dụng src-layout
không chỉ là một lựa chọn về phong cách mà còn là một cơ chế mạnh mẽ để ngăn chặn một lớp lỗi kiểm thử và import phổ biến và tiềm ẩn. Nếu không sử dụng src-layout
và đặt mã nguồn package (ví dụ: package_name/
) trực tiếp tại thư mục gốc của dự án, một vấn đề nghiêm trọng có thể phát sinh. Khi một nhà phát triển chạy các bài kiểm thử từ thư mục gốc, hệ thống import của Python sẽ tự động thêm thư mục làm việc hiện tại vào sys.path
. Điều này tạo ra một sự mơ hồ nguy hiểm: khi một tệp kiểm thử thực thi lệnh import package_name
, Python có thể sẽ import thư mục package_name
cục bộ trực tiếp từ hệ thống tệp, thay vì import phiên bản đã được cài đặt vào môi trường ảo (trong site-packages
).
Hệ quả là các bài kiểm thử có thể vượt qua thành công trên máy cục bộ vì chúng đang kiểm tra các tệp mã nguồn thô, nhưng package lại bị lỗi khi người dùng cuối cài đặt nó. Điều này xảy ra bởi vì quá trình build có thể đã bỏ sót một số tệp cần thiết hoặc cấu hình sai các điểm vào (entry points). Về bản chất, nhà phát triển đang kiểm thử một sản phẩm khác với sản phẩm mà người dùng sẽ cài đặt. Cấu trúc src-layout
giải quyết triệt để vấn đề này. Bằng cách đặt package vào trong src/
, thư mục gốc của dự án không còn nằm trên sys.path
theo cách cho phép import trực tiếp package_name
. Để chạy kiểm thử, nhà phát triển buộc phải thực hiện một “cài đặt có thể chỉnh sửa” (editable install) bằng lệnh pip install -e.
. Quá trình này mô phỏng chính xác cách package sẽ được cài đặt và đảm bảo rằng các bài kiểm thử chạy trên phiên bản đã được cài đặt. Điều này loại bỏ hoàn toàn sự mơ hồ trong việc import và đảm bảo rằng “thứ được kiểm thử chính là thứ được phân phối”, một nguyên tắc nền tảng cho việc kiểm thử đáng tin cậy và có thể tái lập.
1.2. Phân biệt Module và Package
Để tổ chức mã nguồn hiệu quả, việc hiểu rõ sự khác biệt giữa module và package là điều cơ bản.
- Module: Là đơn vị tổ chức mã nguồn nhỏ nhất trong Python. Về cơ bản, một module là một tệp tin duy nhất có phần mở rộng
.py
, chứa các định nghĩa và câu lệnh Python như hàm, lớp, và biến. Mục đích của module là chia nhỏ mã nguồn thành các phần logic, dễ quản lý và có thể tái sử dụng trong các chương trình khác thông qua câu lệnhimport
. - Package: Là một cách để cấu trúc không gian tên module của Python bằng cách sử dụng “tên module có dấu chấm”. Về mặt vật lý, một package là một thư mục chứa các module và một tệp đặc biệt có tên
__init__.py
. Sự hiện diện của tệp__init__.py
báo cho Python biết rằng thư mục đó nên được coi là một package, cho phép tổ chức các module liên quan thành một cấu trúc thư mục có thứ bậc. Một package có thể chứa các package con (sub-packages), tạo ra một cây cấu trúc phức tạp và có tổ chức. Mục đích chính của package là nhóm các module có chức năng liên quan lại với nhau để dễ quản lý và tránh xung đột tên.
1.3. Vai trò Cốt lõi của __init__.py
Tệp __init__.py
là một thành phần trung tâm, xác định danh tính và hành vi của một package trong Python. Mặc dù có thể để trống, vai trò của nó vượt xa một tệp tin đánh dấu đơn thuần.
Vai trò chính: Chức năng cơ bản và quan trọng nhất của __init__.py
là để thông báo cho trình thông dịch Python rằng thư mục chứa nó là một “regular package” (package thông thường). Nếu không có tệp này, trong các phiên bản Python hiện đại (3.3+), thư mục có thể được coi là một “namespace package”, nhưng việc bao gồm __init__.py
một cách tường minh sẽ đảm bảo hành vi nhất quán và là quy ước đã được thiết lập từ lâu. Trong nhiều trường hợp, một tệp __init__.py
rỗng là hoàn toàn đủ để thực hiện vai trò này.
Thực thi khi Import: Mã nguồn bên trong tệp __init__.py
sẽ được thực thi một lần duy nhất vào lần đầu tiên package hoặc bất kỳ module nào bên trong nó được import. Điều này làm cho nó trở thành một nơi lý tưởng để thực hiện các tác vụ khởi tạo ở cấp độ package, chẳng hạn như thiết lập cấu hình, kết nối cơ sở dữ liệu, hoặc định nghĩa các biến toàn cục cho package.
Sử dụng nâng cao – Định nghĩa Public API: Một trong những công dụng mạnh mẽ nhất của __init__.py
là định nghĩa một “Public API” rõ ràng và thuận tiện cho package. Thay vì buộc người dùng phải biết cấu trúc thư mục nội bộ và import từ các submodule cụ thể (ví dụ: from mypackage.math_operations import add
), nhà phát triển có thể “nâng” các đối tượng quan trọng lên không gian tên của package. Bằng cách thêm dòng from.math_operations import add
vào tệp mypackage/__init__.py
, người dùng có thể import trực tiếp và gọn gàng hơn: from mypackage import add
.
Để kiểm soát chặt chẽ hơn những gì được xem là một phần của Public API, biến đặc biệt __all__
có thể được sử dụng. __all__
là một danh sách các chuỗi, liệt kê tên của các đối tượng (module, hàm, lớp) sẽ được import khi người dùng thực hiện một lệnh import ký tự đại diện (from mypackage import *
). Việc định nghĩa __all__
được coi là một thực hành tốt để tránh làm ô nhiễm không gian tên của người dùng và để tường minh hóa giao diện mà package cung cấp.
Tóm lại, __init__.py
hoạt động như một “cánh cửa chính” hay một “mặt tiền” (Facade) cho package, kiểm soát cả danh tính và giao diện công khai của nó. Việc hiểu sai vai trò của nó có thể dẫn đến một API khó sử dụng, rò rỉ các chi tiết triển khai, hoặc thậm chí là thất bại trong việc cấu trúc package một cách đúng đắn. Một __init__.py
được thiết kế tốt sẽ áp dụng mẫu thiết kế Facade, chọn lọc và phơi bày các thành phần chính từ các module nội bộ. Điều này mang lại một lợi ích to lớn về lâu dài: nhà phát triển có thể tự do tái cấu trúc (refactor) cấu trúc nội bộ của package—ví dụ, di chuyển một hàm từ module này sang module khác—mà không làm hỏng mã nguồn của người dùng, miễn là “cánh cửa chính” __init__.py
vẫn tiếp tục phơi bày hàm đó theo cách cũ. Sự tách biệt giữa giao diện công khai và việc triển khai nội bộ này là một nền tảng của thiết kế API tốt và khả năng bảo trì bền vững.
Phần II: Cấu hình và Siêu dữ liệu với pyproject.toml
Hệ sinh thái đóng gói Python đã trải qua một sự chuyển đổi quan trọng, từ các kịch bản thực thi (setup.py
) sang một tiêu chuẩn cấu hình tĩnh, khai báo. Trung tâm của sự chuyển đổi này là tệp pyproject.toml
, một tệp tin không chỉ định nghĩa cách build một package mà còn đang dần trở thành trung tâm điều phối cho toàn bộ công cụ phát triển Python.
2.1. Lịch sử và Sự chuyển dịch từ setup.py
Trong quá khứ, việc đóng gói một dự án Python chủ yếu dựa vào một tệp kịch bản có tên setup.py
. Tệp này là một chương trình Python thực thi, sử dụng thư viện setuptools
để định nghĩa tất cả mọi thứ về package, từ tên, phiên bản cho đến các phụ thuộc. Tuy nhiên, cách tiếp cận này tồn tại nhiều vấn đề cố hữu.
Vấn đề lớn nhất và nguy hiểm nhất của setup.py
là nó yêu cầu thực thi mã nguồn tùy ý (arbitrary code execution). Để một công cụ build như pip
có thể xác định siêu dữ liệu hoặc danh sách phụ thuộc của một package, nó phải chạy tệp setup.py
. Điều này tạo ra một lỗ hổng bảo mật nghiêm trọng, vì nó tương đương với việc chạy một đoạn mã không tin cậy được tải về từ Internet.
Một vấn đề khác là bài toán “con gà và quả trứng” (chicken-and-egg problem). Một tệp setup.py
có thể tự nó có các phụ thuộc; ví dụ, nó có thể import numpy
để lấy thông tin phiên bản. Công cụ build không thể biết rằng nó cần cài đặt numpy
trước khi chạy setup.py
, dẫn đến lỗi build.
Để giải quyết những vấn đề này, cộng đồng Python đã giới thiệu pyproject.toml
thông qua các PEP (Python Enhancement Proposal). Đây là một tệp cấu hình tĩnh, có tính khai báo, sử dụng định dạng TOML (Tom’s Obvious, Minimal Language) dễ đọc.
- PEP 518 đã chuẩn hóa bảng
[build-system]
, cho phép một package khai báo các phụ thuộc cần thiết cho quá trình build của chính nó. Điều này đã giải quyết triệt để bài toán “con gà và quả trứng”. - PEP 621 tiếp tục chuẩn hóa bảng
[project]
, cung cấp một cách tĩnh và an toàn để khai báo siêu dữ liệu của dự án (tên, phiên bản, tác giả, v.v.). Điều này loại bỏ hoàn toàn nhu cầu phải thực thi mã nguồn để đọc thông tin cơ bản của package.
Vậy setup.py
có bị loại bỏ hoàn toàn không? Câu trả lời là không, nhưng vai trò của nó đã thay đổi đáng kể. Việc gọi trực tiếp python setup.py install
hoặc python setup.py upload
đã lỗi thời và không được khuyến khích. Tuy nhiên, setup.py
vẫn có thể tồn tại song song với pyproject.toml
để xử lý các kịch bản phức tạp mà cấu hình tĩnh không thể đáp ứng, ví dụ điển hình nhất là việc build các phần mở rộng C/C++ (C extensions). Đối với phần lớn các dự án, đặc biệt là các dự án thuần Python, pyproject.toml
có thể và nên thay thế hoàn toàn setup.py
và setup.cfg
.
Bảng dưới đây tóm tắt sự phát triển của các tệp cấu hình trong việc đóng gói Python, làm rõ lý do tại sao pyproject.toml
là tiêu chuẩn hiện đại được khuyến nghị.
Thuộc tính | setup.py (Di sản) | setup.cfg (Chuyển tiếp) | pyproject.toml (Tiêu chuẩn hiện đại) |
Định dạng | Kịch bản Python | Tệp INI | Tệp TOML |
Bản chất | Mệnh lệnh (Imperative) | Khai báo (Declarative) | Khai báo (Declarative) |
Bảo mật | Rủi ro thực thi mã tùy ý | An toàn để phân tích cú pháp | An toàn để phân tích cú pháp |
Tiêu chuẩn hóa | De facto (thực tế) | Dành riêng cho setuptools | Tiêu chuẩn PEP (518, 621) |
Khai báo phụ thuộc build | Qua setup_requires (lỗi thời) | Cần setup.py tối thiểu | Qua bảng [build-system] |
Khuyến nghị hiện tại | Tránh sử dụng trực tiếp | Chỉ dùng cho C extensions nếu cần | Khuyến nghị mạnh mẽ |
2.2. Giải phẫu file pyproject.toml
Tệp pyproject.toml
được cấu trúc thành các bảng (tables) theo cú pháp TOML. Có ba bảng chính được định nghĩa bởi các tiêu chuẩn đóng gói:
1. Bảng [build-system]:
Đây là bảng đầu tiên mà một công cụ build frontend (như pip hoặc build) sẽ đọc. Nó có vai trò khai báo hệ thống build backend và các yêu cầu của nó.
requires
: Một danh sách các package cần thiết để build dự án của bạn. Tối thiểu, nó phải chứa chính build backend. Ví dụ:requires = ["setuptools>=61.0", "wheel"]
.build-backend
: Một chuỗi chỉ định đối tượng Python mà các công cụ frontend sẽ gọi để thực hiện quá trình build. Ví dụ:build-backend = "setuptools.build_meta"
.
2. Bảng [project]:
Bảng này chứa tất cả các siêu dữ liệu tĩnh, được chuẩn hóa về dự án của bạn, tuân theo đặc tả của PEP 621. Đây là nơi bạn định nghĩa tên, phiên bản, mô tả, tác giả, các phụ thuộc, và nhiều thông tin khác. Chi tiết về các trường trong bảng này sẽ được trình bày ở mục 2.3.
3. Bảng [tool]:
Đây là một không gian tên (namespace) dành riêng cho cấu hình của các công cụ cụ thể. Mỗi công cụ sẽ có một bảng con riêng, ví dụ [tool.pytest], [tool.black], [tool.hatch].
Sự ra đời của bảng [tool]
là một bước tiến quan trọng, biến pyproject.toml
từ một tệp tin chỉ dành cho việc đóng gói thành một trung tâm cấu hình hợp nhất cho toàn bộ hệ sinh thái công cụ phát triển Python. Trước đây, một dự án thường có vô số các tệp cấu hình rải rác: setup.py
cho đóng gói, tox.ini
cho tự động hóa kiểm thử, .coveragerc
cho độ bao phủ mã, .flake8
cho linting, v.v.. Điều này tạo ra sự lộn xộn và khó khăn cho các nhà phát triển mới khi tiếp cận một dự án.
Bảng [tool]
đã giải quyết vấn đề này bằng cách cung cấp một vị trí duy nhất, được chuẩn hóa để các công cụ như pytest
, black
, ruff
, và mypy
lưu trữ cấu hình của chúng. Một nhà phát triển giờ đây chỉ cần nhìn vào một tệp duy nhất—pyproject.toml
—để hiểu không chỉ siêu dữ liệu và phụ thuộc của package, mà còn cả cách nó được kiểm thử, định dạng, và kiểm tra chất lượng mã nguồn. Điều này giúp đơn giản hóa đáng kể việc thiết lập dự án, thúc đẩy tính nhất quán, và biến pyproject.toml
thành một “bản kê khai dự án” (project manifest) thực thụ.
2.3. Khai báo Siêu dữ liệu (Metadata) trong [project]
Bảng [project]
là trái tim của việc cấu hình package theo tiêu chuẩn hiện đại. Việc điền đầy đủ và chính xác các thông tin trong bảng này là rất quan trọng để package của bạn được nhận diện, tìm kiếm và sử dụng một cách đúng đắn trên PyPI và bởi các công cụ như pip
. Dưới đây là giải thích chi tiết về các trường quan trọng nhất:
name
: Tên phân phối (distribution name) của package trên PyPI. Tên này phải là duy nhất và chỉ được chứa chữ cái, số, và các ký tự.
(dấu chấm),_
(gạch dưới),-
(gạch nối).version
: Phiên bản của package. Việc tuân theo Đánh số phiên bản ngữ nghĩa (Semantic Versioning) được khuyến nghị mạnh mẽ. Phiên bản có thể được khai báo tĩnh (ví dụ:version = "0.1.0"
) hoặc động, nghĩa là được đọc từ một tệp tin hoặc từ thẻ git, một tính năng được hỗ trợ bởi một số build backend.description
: Một bản tóm tắt ngắn gọn, một câu về mục đích của package.readme
: Đường dẫn tương đối đến tệp mô tả chi tiết của package (thường làREADME.md
). Nội dung của tệp này sẽ được hiển thị trên trang chi tiết của dự án trên PyPI. Có thể chỉ định loại nội dung để PyPI hiển thị đúng định dạng, ví dụ:readme = {file = "README.md", content-type = "text/markdown"}
.requires-python
: Chuỗi chỉ định các phiên bản Python mà dự án của bạn hỗ trợ, ví dụ:requires-python = ">=3.8"
.pip
sẽ sử dụng thông tin này để chọn phiên bản package phù hợp hoặc từ chối cài đặt nếu phiên bản Python của người dùng không tương thích.license
: Một chuỗi chứa biểu thức giấy phép theo chuẩn SPDX (ví dụ:"MIT"
,"Apache-2.0 OR BSD-3-Clause"
). Việc sử dụng các classifierLicense ::
đã lỗi thời và nên được thay thế bằng trường này.license-files
: Một danh sách các mẫu glob (glob pattern) cho các tệp giấy phép cần được đưa vào bản phân phối, ví dụ:license-files =
.authors
/maintainers
: Một mảng các bảng, mỗi bảng chứa hai khóa làname
(tên) vàemail
(thư điện tử) của tác giả hoặc người bảo trì.keywords
: Một danh sách các từ khóa giúp người dùng dễ dàng tìm thấy package của bạn trên PyPI.classifiers
: Một danh sách các “trove classifiers” từ PyPI để phân loại dự án. Đây là siêu dữ liệu cực kỳ quan trọng, giúp lọc và tìm kiếm package. Các classifier quan trọng cần có bao gồm phiên bản Python tương thích, giấy phép, và hệ điều hành hỗ trợ (ví dụ:"Programming Language :: Python :: 3"
,"License :: OSI Approved :: MIT License"
,"Operating System :: OS Independent"
).urls
: Một bảng chứa các liên kết hữu ích liên quan đến dự án, chẳng hạn nhưHomepage
,Documentation
,Repository
,Changelog
.scripts
/entry_points
: Định nghĩa các kịch bản dòng lệnh (command-line scripts) sẽ được tạo ra khi người dùng cài đặt package, cho phép họ chạy các chức năng của package trực tiếp từ terminal.
Phần III: Quản lý Dependencies một cách Chuyên nghiệp
Quản lý phụ thuộc (dependency management) là một trong những khía cạnh phức tạp và dễ gây ra sự cố nhất trong phát triển phần mềm. Một chiến lược quản lý phụ thuộc không tốt có thể dẫn đến “địa ngục phụ thuộc” (dependency hell), nơi các xung đột phiên bản không thể giải quyết làm tê liệt dự án. Phần này sẽ trình bày các phương pháp chuyên nghiệp để khai báo và quản lý phụ thuộc bằng pyproject.toml
.
3.1. Dependencies Cốt lõi và Dependencies Tùy chọn
Việc phân loại phụ thuộc một cách rõ ràng là bước đầu tiên để có một hệ thống quản lý vững chắc.
Dependencies Cốt lõi (dependencies
): Đây là danh sách các package mà dự án của bạn bắt buộc phải có để hoạt động. Nếu thiếu bất kỳ phụ thuộc nào trong danh sách này, package của bạn sẽ không thể chạy được. Các phụ thuộc này được khai báo trong khóa dependencies
của bảng [project]
trong pyproject.toml
và sẽ được pip
tự động cài đặt cùng với package của bạn.
Ini, TOML
[project]
#...
dependencies = [
"requests >= 2.20.0",
"rich",
]
Dependencies Tùy chọn (optional-dependencies
): Đây là các package không cần thiết cho chức năng cốt lõi nhưng lại kích hoạt các tính năng bổ sung. Chúng được khai báo trong một bảng riêng có tên [project.optional-dependencies]
. Cơ chế này cho phép người dùng “chọn tham gia” (opt-in) vào các tính năng mà họ cần, giữ cho việc cài đặt mặc định được gọn nhẹ.
Ví dụ, một thư viện xử lý dữ liệu có thể cung cấp chức năng xuất file PDF, nhưng không phải người dùng nào cũng cần đến nó. Thay vì bắt tất cả mọi người phải cài đặt thư viện tạo PDF, ta có thể định nghĩa nó như một phụ thuộc tùy chọn:
Ini, TOML
[project.optional-dependencies]
pdf = ["reportlab>=3.5.0"]
excel = ["openpyxl"]
Người dùng muốn có chức năng xuất PDF có thể cài đặt bằng lệnh: pip install your-package[pdf]
. Họ cũng có thể cài đặt nhiều nhóm cùng lúc: pip install your-package[pdf,excel]
.
Cơ chế này cũng là một cách hiệu quả để quản lý các phụ thuộc chỉ dành cho môi trường phát triển, kiểm thử, hoặc xây dựng tài liệu. Một quy ước phổ biến là tạo các nhóm test
, docs
, và dev
:
Ini, TOML
[project.optional-dependencies]
test = ["pytest", "pytest-cov"]
docs = ["sphinx"]
dev = [
"your-package[test,docs]",
"black",
"ruff",
]
Với cấu hình này, một nhà phát triển có thể thiết lập môi trường làm việc đầy đủ chỉ bằng một lệnh duy nhất: pip install -e ".[dev]"
.
3.2. Chỉ định Phiên bản (Version Specifiers) và Semantic Versioning
Cách bạn chỉ định phiên bản cho các phụ thuộc của mình có ảnh hưởng sâu sắc đến sự ổn định và linh hoạt của cả package của bạn và các dự án sử dụng nó.
Semantic Versioning (SemVer): Đây là quy ước đặt tên phiên bản được khuyến nghị mạnh mẽ, có định dạng MAJOR.MINOR.PATCH
.
MAJOR
(ví dụ:2.0.0
): Tăng khi có những thay đổi phá vỡ tính tương thích ngược (breaking changes).MINOR
(ví dụ:1.3.0
): Tăng khi thêm chức năng mới nhưng vẫn đảm bảo tương thích ngược.PATCH
(ví dụ:1.2.1
): Tăng khi có các bản vá lỗi và vẫn đảm bảo tương thích ngược.
Việc tuân thủ SemVer không chỉ là một quy tắc kỹ thuật mà còn là một “hợp đồng” với người dùng, giúp truyền đạt rõ ràng về tác động của các bản cập nhật, cho phép họ nâng cấp một cách an toàn.
PEP 440 Version Specifiers: Python định nghĩa một cú pháp phong phú để chỉ định các ràng buộc phiên bản trong tệp pyproject.toml
.
~=
(Compatible release operator): Đây là toán tử được khuyến nghị nhiều nhất cho các thư viện.~=1.2.3
tương đương với>=1.2.3, <1.3.0
.- ~=1.2 tương đương với >=1.2, <2.0.Toán tử này cho phép tự động nhận các bản vá lỗi (thay đổi PATCH) nhưng ngăn chặn các bản cập nhật có thể gây ra lỗi do thay đổi API (thay đổi MINOR hoặc MAJOR).
>=
,<=
,>
,<
: Các toán tử so sánh phiên bản tiêu chuẩn.==
: Chỉ định một phiên bản chính xác tuyệt đối.!=
: Loại trừ một phiên bản cụ thể.
Thực hành tốt nhất:
- Đối với Thư viện (Libraries): Không bao giờ ghim phiên bản chính xác (
==
) cho các phụ thuộc trongdependencies
. Đây là một trong những sai lầm nghiêm trọng nhất trong việc đóng gói. Việc này tạo ra các ràng buộc cứng nhắc, dễ dàng dẫn đến xung đột khi một ứng dụng cố gắng sử dụng hai thư viện mà cả hai đều yêu cầu các phiên bản khác nhau của một phụ thuộc thứ ba. Thay vào đó, hãy sử dụng các ràng buộc linh hoạt như~=
hoặc>=
để cho phép hệ sinh thái tự giải quyết phiên bản phù hợp. - Đối với Ứng dụng (Applications): Ngược lại, đối với các ứng dụng cuối, việc khóa (lock) tất cả các phụ thuộc vào các phiên bản chính xác là một thực hành tốt nhất. Điều này đảm bảo môi trường có thể tái lập một cách hoàn hảo, nghĩa là mọi nhà phát triển và mọi môi trường triển khai (staging, production) đều sử dụng cùng một bộ phiên bản package, loại bỏ các lỗi do sự khác biệt về phiên bản. Việc này thường được thực hiện thông qua một tệp khóa như
poetry.lock
hoặcpdm.lock
được tạo ra bởi các công cụ quản lý workflow, hoặc bằng cách sử dụngpip-tools
để tạo một tệprequirements.txt
đã được ghim hoàn toàn từ một tệprequirements.in
trừu tượng hơn.
3.3. Quản lý Dependencies cho Môi trường Phát triển
Việc quản lý các công cụ và thư viện chỉ cần thiết cho quá trình phát triển là một phần quan trọng của một quy trình làm việc chuyên nghiệp.
Bảng [project.optional-dependencies]
là phương pháp chuẩn để định nghĩa các nhóm phụ thuộc này. Như đã đề cập, các nhóm phổ biến là dev
, test
, và docs
. Một nhà phát triển có thể dễ dàng cài đặt chúng bằng lệnh pip install -e.[dev]
.
Sự chuẩn hóa này đang thay đổi cơ bản cách các dự án Python được khởi tạo và cách các quy trình Tích hợp/Triển khai Liên tục (CI/CD) được cấu hình. Trước đây, một tệp CONTRIBUTING.md
thường chứa một loạt các bước thiết lập thủ công: tạo môi trường ảo, kích hoạt nó, rồi chạy pip install
trên nhiều tệp requirements
khác nhau. Quy trình này dễ xảy ra lỗi và rườm rà.
Với cách tiếp cận hiện đại, pyproject.toml
trở thành nguồn chân lý duy nhất (single source of truth). Hướng dẫn thiết lập cho một người đóng góp mới được đơn giản hóa thành một lệnh duy nhất. Điều này cũng có tác động lớn đến CI/CD. Cấu hình CI (ví dụ: .github/workflows/ci.yml
) không còn cần phải phân tích nhiều tệp requirements
nữa. Nó có thể chỉ cần chạy pip install.[test]
để có được chính xác các phụ thuộc cần thiết cho bộ kiểm thử. Điều này làm cho cấu hình CI sạch hơn, đáng tin cậy hơn và liên kết chặt chẽ hơn với định nghĩa của chính package về môi trường của nó. Cách tiếp cận này khuyến khích các nhà phát triển suy nghĩ về dự án của họ không chỉ như là mã nguồn, mà là một thực thể tự chứa, có thể mô tả được. Nó cải thiện khả năng tái lập, giảm rào cản cho những người đóng góp mới, và đơn giản hóa các quy trình tự động.
Các công cụ quản lý workflow như Poetry, Hatch, và PDM còn cung cấp các cơ chế quản lý các nhóm phụ thuộc này một cách tiên tiến hơn. Ví dụ, Poetry sử dụng một bảng [tool.poetry.group.dev.dependencies]
riêng biệt, tách bạch rõ ràng các phụ thuộc phát triển khỏi các “extra” có thể cài đặt bởi người dùng. Hatch cung cấp một tính năng ma trận môi trường (environment matrix) mạnh mẽ, có thể thay thế hoàn toàn các công cụ như tox
để chạy kiểm thử trên nhiều phiên bản Python khác nhau.
Phần IV: Xây dựng và Đóng gói
Sau khi mã nguồn được viết và dự án được cấu hình, bước tiếp theo là biến chúng thành các “hiện vật” (artifacts) có thể phân phối được. Quá trình này được gọi là “build” hoặc “packaging”, và nó tạo ra các tệp tin chuẩn hóa mà các công cụ như pip
có thể hiểu và cài đặt.
4.1. Source Distributions (sdist) vs. Wheels (whl)
Trong hệ sinh thái Python hiện đại, có hai định dạng phân phối chính mà bạn cần tạo ra:
1. Source Distribution (sdist):
- Đây là một bản phân phối mã nguồn, thường được đóng gói dưới dạng một tệp lưu trữ nén
.tar.gz
. - Nó chứa mã nguồn Python thô (
.py
), tệppyproject.toml
, và tất cả các tệp khác cần thiết để xây dựng (build) package từ đầu. - Khi một người dùng cài đặt từ một
sdist
,pip
sẽ phải thực hiện một bước build trên máy của họ. Nếu package chứa các phần mở rộng C/C++, bước này đòi hỏi người dùng phải có sẵn trình biên dịch (compiler) và các tệp tiêu đề phát triển (development headers) tương ứng, điều này thường gây ra lỗi và là một rào cản lớn cho người dùng không chuyên.
2. Wheel (whl):
- Đây là một bản phân phối đã được build sẵn (built distribution). Về cơ bản, nó là một tệp lưu trữ ZIP (
.zip
) với phần mở rộng là.whl
. - Nó chứa các tệp đã được chuẩn bị sẵn sàng để cài đặt trực tiếp vào thư mục
site-packages
mà không cần bất kỳ bước build nào trên máy người dùng. Đối với các package chứa mã biên dịch, tệp wheel sẽ bao gồm các tệp nhị phân đã được biên dịch sẵn cho một nền tảng (hệ điều hành và kiến trúc CPU) và phiên bản Python cụ thể. - Wheel là định dạng được ưu tiên cho việc phân phối và cài đặt vì những lợi ích rõ rệt của nó:
- Cài đặt nhanh hơn: Bỏ qua bước build tốn thời gian.
- Kích thước nhỏ hơn: Thường có kích thước nhỏ hơn sdist.
- Đáng tin cậy hơn: Loại bỏ nhiều biến số có thể gây lỗi trong quá trình build trên máy người dùng (ví dụ: thiếu trình biên dịch).
- An toàn hơn: Tránh việc thực thi mã tùy ý từ
setup.py
trong quá trình cài đặt.
Mặc dù wheel là định dạng ưu tiên, việc cung cấp cả sdist
là một thực hành tốt nhất và gần như là bắt buộc. Bạn nên luôn luôn xuất bản một tệp sdist cùng với các tệp wheel của mình. Lý do là sdist
đóng vai trò như một kho lưu trữ mã nguồn chính tắc (canonical source archive). Nó cho phép người dùng trên các nền tảng mà bạn không cung cấp wheel (ví dụ: một kiến trúc CPU hiếm) có thể tự mình thử build package. Hơn nữa, nhiều công cụ đóng gói hạ nguồn (downstream packagers) như conda-forge
và các nhà phân phối Linux yêu cầu sdist
để có thể build lại package cho hệ thống của họ.
4.2. Hệ thống Build Backend
Build backend là công cụ thực sự thực hiện công việc tạo ra các tệp sdist
và wheel
. Nó được chỉ định trong bảng [build-system]
của pyproject.toml
và được gọi bởi một công cụ build frontend. Sự tách biệt giữa frontend (giao diện người dùng, như pip
hoặc build
) và backend (công cụ thực thi, như setuptools
) là một nguyên tắc thiết kế cốt lõi của hệ thống đóng gói hiện đại, cho phép sự linh hoạt và đổi mới.
Việc lựa chọn build backend phụ thuộc vào độ phức tạp của dự án:
- Đối với các package thuần Python: Có nhiều lựa chọn phổ biến và phần lớn chúng có thể thay thế cho nhau cho các dự án đơn giản. Các lựa chọn bao gồm
setuptools
,hatchling
,flit-core
, vàpdm-backend
. - Đối với các package có phần mở rộng biên dịch: Cần các backend chuyên dụng hơn.
setuptools
: Là lựa chọn truyền thống và vẫn là lựa chọn mạnh mẽ nhất, linh hoạt nhất cho các phần mở rộng C/C++ phức tạp.scikit-build-core
: Một backend hiện đại dành cho các dự án sử dụng CMake để build.meson-python
: Một backend hiện đại dành cho các dự án sử dụng Meson, được các dự án lớn như SciPy và NumPy áp dụng.maturin
: Backend chuyên dụng để build các phần mở rộng được viết bằng ngôn ngữ Rust.
4.3. Quy trình Build với python -m build
build
là một dự án của PyPA (Python Packaging Authority) đóng vai trò là một công cụ build frontend đơn giản và tuân thủ tiêu chuẩn. Nó là công cụ được khuyến nghị để thực hiện quá trình build một cách thủ công.
Quy trình thực hiện như sau:
- Cài đặt
build
:pip install build
- Chạy quá trình build:Từ thư mục gốc của dự án (nơi chứa tệp pyproject.toml), thực thi lệnh:
python -m build
Lệnh này sẽ thực hiện các bước sau một cách tự động:
- Đọc tệp
pyproject.toml
để xác định build backend và các phụ thuộc build cần thiết. - Tạo một môi trường build ảo, cô lập và cài đặt các phụ thuộc đó vào.
- Gọi build backend trong môi trường cô lập để lần lượt tạo ra một tệp
sdist
(.tar.gz
) và một tệpwheel
(.whl
). - Lưu các tệp kết quả vào một thư mục mới có tên là
dist/
tại thư mục gốc của dự án.
Một lưu ý quan trọng là cần đảm bảo môi trường build luôn sạch sẽ. Các công cụ build cũ hơn có thể bị ảnh hưởng bởi các tệp tin còn sót lại từ các lần build trước. Do đó, một thực hành tốt là luôn xóa thư mục build/
(thư mục làm việc tạm thời của backend) trước khi chạy lại quá trình build để tránh các lỗi không mong muốn. Công cụ build
hiện đại thường xử lý việc này tốt hơn bằng cách sử dụng môi trường cô lập, nhưng việc dọn dẹp vẫn là một thói quen tốt.
Phần V: Các Thành phần Thiết yếu cho một Package Chất lượng cao
Một package chuyên nghiệp không chỉ chứa mã nguồn hoạt động tốt. Nó còn phải được kiểm thử kỹ lưỡng, có tài liệu rõ ràng, và tuân thủ các quy ước pháp lý và cộng đồng. Những thành phần này không phải là tùy chọn; chúng là yếu tố phân biệt giữa một dự án cá nhân và một sản phẩm phần mềm đáng tin cậy.
5.1. Kiểm thử Tự động với pytest
Kiểm thử tự động là nền tảng của phát triển phần mềm hiện đại. Nó giúp phát hiện lỗi sớm, cho phép tái cấu trúc mã nguồn một cách tự tin, và đảm bảo chất lượng của package qua thời gian.
Mặc dù unittest
là một phần của thư viện chuẩn, pytest
đã trở thành tiêu chuẩn thực tế trong cộng đồng Python nhờ cú pháp đơn giản, các tính năng mạnh mẽ và một hệ sinh thái plugin phong phú. pytest
sử dụng câu lệnh assert
gốc của Python, giúp các bài kiểm thử trở nên ngắn gọn và dễ đọc hơn nhiều so với các phương thức self.assert*
của unittest
.
Thiết lập pytest
:
- Cài đặt:
pip install pytest pytest-cov
(pytest-cov
là plugin để đo độ bao phủ mã). - Cấu trúc: Đặt các tệp kiểm thử trong một thư mục
tests/
riêng biệt ở cấp cao nhất của dự án, song song với thư mụcsrc/
. - Quy ước đặt tên:
pytest
sẽ tự động phát hiện các tệp kiểm thử có tên theo mẫutest_*.py
hoặc*_test.py
, và các hàm hoặc phương thức kiểm thử có tên bắt đầu bằngtest_
.
Các thực hành tốt nhất khi viết kiểm thử:
- Độc lập: Mỗi bài kiểm thử phải hoàn toàn độc lập với các bài khác. Nó phải có thể chạy một mình và theo bất kỳ thứ tự nào. Sử dụng các
fixture
củapytest
để thiết lập và dọn dẹp trạng thái cho mỗi bài kiểm thử. - Tốc độ: Các bài kiểm thử nên chạy nhanh. Tốc độ nhanh khuyến khích việc chạy chúng thường xuyên, giúp phát hiện lỗi sớm hơn.
- Rõ ràng: Sử dụng tên hàm kiểm thử dài và có tính mô tả cao, ví dụ:
test_calculate_average_with_empty_list_raises_value_error
. Tên hàm nên mô tả rõ ràng hành vi đang được kiểm thử và kết quả mong đợi. - Tập trung: Mỗi bài kiểm thử chỉ nên xác minh một hành vi logic hoặc một khẳng định duy nhất. Việc kiểm tra nhiều thứ trong một bài kiểm thử sẽ làm cho việc xác định nguyên nhân lỗi trở nên khó khăn.
- Cô lập: Sử dụng kỹ thuật “mocking” (ví dụ: với thư viện
unittest.mock
) để cô lập đơn vị mã đang được kiểm thử khỏi các phụ thuộc bên ngoài như cơ sở dữ liệu, API mạng, hoặc hệ thống tệp. Điều này làm cho các bài kiểm thử nhanh hơn, đáng tin cậy hơn và không phụ thuộc vào môi trường bên ngoài. - Tham số hóa (Parametrization): Khi cần kiểm thử cùng một hàm với nhiều bộ dữ liệu đầu vào và đầu ra khác nhau, hãy sử dụng decorator
@pytest.mark.parametrize
để tránh lặp lại mã và làm cho bộ kiểm thử trở nên gọn gàng, dễ bảo trì. - Độ bao phủ (Coverage): Sử dụng các công cụ như
pytest-cov
để đo lường tỷ lệ mã nguồn được thực thi bởi các bài kiểm thử. Mặc dù độ bao phủ 100% không đảm bảo mã không có lỗi, nhưng nó là một chỉ số hữu ích để xác định các phần của mã nguồn chưa được kiểm thử. Hãy tập trung vào việc kiểm thử các logic quan trọng, các trường hợp biên (edge cases) và xử lý lỗi.
5.2. Tài liệu hóa Dự án (Project Documentation)
Tài liệu là giao diện giao tiếp giữa package của bạn và người dùng. Một package dù mạnh mẽ đến đâu cũng sẽ trở nên vô dụng nếu không có tài liệu hướng dẫn rõ ràng.
Các thành phần chính của tài liệu:
README.md
: Đây là “trang chủ” của dự án trên các nền tảng như GitHub và PyPI. Nó phải chứa một bản tóm tắt súc tích về mục đích của package, hướng dẫn cài đặt, một ví dụ sử dụng cơ bản (“quick start”), và các liên kết đến tài liệu đầy đủ, trình theo dõi lỗi (issue tracker), và hướng dẫn đóng góp.- Docstrings: Đây là các chuỗi tài liệu được nhúng trực tiếp vào mã nguồn (trong các module, lớp, hàm). Chúng là nguồn thông tin chính cho hàm
help()
của Python và là nền tảng để các công cụ tự động tạo tài liệu API. Các định dạng docstring phổ biến bao gồm Google-style, NumPy-style, và Sphinx-style (sử dụng reStructuredText). - Tài liệu đầy đủ (Full Documentation): Một trang web tài liệu riêng biệt, thường được host trên các dịch vụ như Read the Docs hoặc GitHub Pages. Trang này chứa các hướng dẫn chi tiết (tutorials), hướng dẫn theo chủ đề (guides), và tài liệu tham khảo API (API reference) được tạo tự động từ docstrings.
Các công cụ tạo tài liệu:
Có hai công cụ chính thống trị trong hệ sinh thái Python để tạo ra các trang web tài liệu tĩnh:
- Sphinx: Là công cụ truyền thống, mạnh mẽ và cực kỳ linh hoạt. Ban đầu được tạo ra cho tài liệu của chính Python, Sphinx có thể tạo ra nhiều định dạng đầu ra (HTML, PDF, ePub), có khả năng tham chiếu chéo mạnh mẽ, và hỗ trợ tuyệt vời cho việc tự động tạo tài liệu API từ docstrings thông qua tiện ích mở rộng
autodoc
. Ngôn ngữ đánh dấu mặc định của nó là reStructuredText (.rst
), một ngôn ngữ mạnh mẽ nhưng có thể khó học hơn Markdown. Tuy nhiên, Sphinx cũng có thể được cấu hình để sử dụng Markdown thông qua tiện ích MyST. - MkDocs: Là một lựa chọn hiện đại, nhanh chóng và đơn giản hơn. MkDocs sử dụng hoàn toàn ngôn ngữ Markdown, giúp giảm rào cản cho những người đã quen thuộc với nó. Nó đi kèm với một máy chủ phát triển tích hợp có khả năng tự động tải lại trang khi có thay đổi, mang lại trải nghiệm phát triển mượt mà. Khi kết hợp với theme
mkdocs-material
và pluginmkdocstrings
, MkDocs có thể tạo ra các trang tài liệu hiện đại, đẹp mắt và có đầy đủ tài liệu tham khảo API được trích xuất từ docstrings.
Bảng dưới đây cung cấp một so sánh trực quan giữa Sphinx và MkDocs để giúp lựa chọn công cụ phù hợp.
Thuộc tính | Sphinx | MkDocs |
Ngôn ngữ đánh dấu chính | reStructuredText (mặc định), Markdown (qua MyST) | Markdown |
Độ dễ cài đặt/sử dụng | Phức tạp hơn, nhiều cấu hình | Rất đơn giản, nhanh chóng |
Tự động tạo tài liệu API | Hỗ trợ tích hợp xuất sắc (autodoc ) | Yêu cầu plugin (mkdocstrings ) |
Định dạng đầu ra | HTML, PDF, ePub, và nhiều định dạng khác | Chỉ HTML (có plugin cho các định dạng khác) |
Xem trước trực tiếp | Không (cần plugin) | Có (tích hợp sẵn) |
Hệ sinh thái & Mở rộng | Rất lớn, trưởng thành | Đang phát triển, tốt |
Phù hợp nhất cho | Các dự án lớn, phức tạp, thư viện khoa học, yêu cầu nhiều định dạng đầu ra | Hầu hết các dự án khác, tài liệu API, ưu tiên sự đơn giản và Markdown |
5.3. Cấp phép và Bản quyền (Licensing and Copyright)
Một package không có giấy phép không phải là phần mềm tự do. Theo luật bản quyền, nếu không có giấy phép nào được chỉ định, người khác không có quyền hợp pháp để sử dụng, sửa đổi hoặc phân phối lại mã nguồn của bạn. Việc chọn một giấy phép là một bước bắt buộc để chia sẻ package của bạn.
Lựa chọn giấy phép:
Các trang web như choosealicense.com là nguồn tài nguyên tuyệt vời để giúp bạn lựa chọn. Các giấy phép mã nguồn mở thường được chia thành hai loại chính:
- Giấy phép Dễ dãi (Permissive): Các giấy phép như MIT, BSD, và Apache 2.0 cho phép người dùng làm gần như mọi thứ với mã nguồn, bao gồm cả việc sử dụng nó trong các phần mềm thương mại, độc quyền, miễn là họ giữ lại thông báo bản quyền và giấy phép gốc. Giấy phép MIT là một lựa chọn rất phổ biến vì sự đơn giản và rõ ràng của nó, đặc biệt phù hợp cho các thư viện.
- Giấy phép Copyleft: Các giấy phép như GNU General Public License (GPL) yêu cầu rằng bất kỳ tác phẩm phái sinh nào (chương trình sử dụng hoặc sửa đổi mã nguồn gốc) cũng phải được phát hành dưới cùng một giấy phép copyleft. Điều này đảm bảo rằng mã nguồn và các sửa đổi của nó luôn luôn là mã nguồn mở. Đặc tính này đôi khi được gọi là “tính lan truyền” (viral) của GPL.
Cách áp dụng giấy phép cho dự án:
- Tạo tệp
LICENSE
: Đặt toàn bộ văn bản của giấy phép bạn đã chọn vào một tệp có tênLICENSE
(hoặcLICENSE.txt
) ở thư mục gốc của dự án. - Khai báo trong
pyproject.toml
: Sử dụng các khóalicense
vàlicense-files
trong bảng[project]
.- Khóa
license
nên chứa một biểu thức giấy phép theo chuẩn SPDX, ví dụ:license = "MIT"
hoặclicense = "GPL-3.0-or-later"
. - Khóa
license-files
nên trỏ đến tệp giấy phép của bạn, ví dụ:license-files =
.
- Khóa
5.4. Các file quan trọng khác
Ngoài mã nguồn, kiểm thử, tài liệu và giấy phép, một dự án chuyên nghiệp còn cần một số tệp tin khác để tạo điều kiện thuận lợi cho việc quản lý và đóng góp của cộng đồng.
.gitignore
: Đây là tệp cấu hình cho Git, chỉ định các tệp và thư mục cần được bỏ qua và không đưa vào hệ thống quản lý phiên bản. Các mục cần thiết phải có trong.gitignore
bao gồm: thư mục môi trường ảo (.venv/
,venv/
,__pypackages__/
), các sản phẩm build (build/
,dist/
,*.egg-info/
), và các tệp cache của Python (__pycache__/
,*.pyc
).CODE_OF_CONDUCT.md
: Bộ quy tắc ứng xử định nghĩa các tiêu chuẩn cộng đồng và hướng dẫn về cách tương tác một cách tôn trọng. Đây là một tài liệu quan trọng để xây dựng một môi trường đóng góp lành mạnh, thân thiện và hòa nhập. Contributor Covenant là một mẫu phổ biến và được khuyến nghị rộng rãi.CONTRIBUTING.md
: Tệp này cung cấp hướng dẫn chi tiết cho những người muốn đóng góp vào dự án. Nó nên bao gồm các thông tin như cách thiết lập môi trường phát triển, cách chạy bộ kiểm thử, quy trình gửi một “pull request”, và các tiêu chuẩn về mã nguồn (coding standards).
Phần VI: Phân phối và Xuất bản lên PyPI
Sau khi package đã được xây dựng và kiểm thử kỹ lưỡng, bước cuối cùng là chia sẻ nó với thế giới. Kho lưu trữ package chính thức cho cộng đồng Python là Python Package Index (PyPI). Phần này sẽ hướng dẫn quy trình xuất bản package một cách an toàn và hiệu quả.
6.1. Chuẩn bị Xuất bản
Trước khi tải package lên, có một vài bước chuẩn bị quan trọng cần thực hiện để đảm bảo quá trình diễn ra suôn sẻ và an toàn.
Tạo tài khoản:
Bạn sẽ cần tạo tài khoản trên hai nền tảng:
- PyPI (pypi.org): Đây là kho lưu trữ chính thức, nơi package của bạn sẽ được công khai và người dùng trên toàn thế giới có thể cài đặt bằng
pip
. - TestPyPI (test.pypi.org): Đây là một bản sao của PyPI dùng cho mục đích thử nghiệm. Luôn luôn tải package của bạn lên TestPyPI trước để kiểm tra xem mọi thứ có hoạt động đúng như mong đợi không—từ việc cài đặt, import cho đến cách siêu dữ liệu được hiển thị trên trang web. Điều này giúp bạn tránh việc “làm bẩn” kho lưu trữ chính thức bằng các phiên bản thử nghiệm hoặc bị lỗi.
Bảo mật tài khoản:
Bảo mật tài khoản PyPI của bạn là cực kỳ quan trọng, vì việc chiếm đoạt tài khoản có thể cho phép kẻ xấu phát hành các phiên bản độc hại của package của bạn.
- Xác thực hai yếu tố (2FA): Bật 2FA cho tài khoản PyPI của bạn là một biện pháp bảo vệ bắt buộc. Bạn có thể sử dụng một ứng dụng xác thực hỗ trợ chuẩn TOTP (như Google Authenticator, Authy) hoặc một khóa bảo mật vật lý (security key).
- Sử dụng API Token: Thay vì sử dụng tên người dùng và mật khẩu trực tiếp khi tải package lên, hãy tạo một API token. API token là các chuỗi ký tự dài, duy nhất mà bạn có thể tạo ra từ trang cài đặt tài khoản PyPI. Bạn có thể giới hạn phạm vi (scope) của một token, ví dụ, chỉ cho phép nó tải lên các phiên bản của một dự án cụ thể. Điều này an toàn hơn nhiều so với việc sử dụng mật khẩu chính của bạn.
6.2. Sử dụng twine
để Upload an toàn
twine
là công cụ dòng lệnh được khuyến nghị chính thức bởi PyPA để tải các bản phân phối package lên PyPI.
Tại sao phải dùng twine
?
- Bảo mật:
twine
luôn sử dụng kết nối HTTPS được xác minh để giao tiếp với PyPI, bảo vệ thông tin đăng nhập của bạn khỏi bị nghe lén. Các phương pháp cũ hơn nhưpython setup.py upload
không đảm bảo được điều này và đã lỗi thời. - Tách biệt Build và Upload:
twine
hoạt động trên các tệp phân phối đã được build sẵn (sdist
vàwheel
trong thư mụcdist/
). Điều này cho phép bạn kiểm tra chính xác các tệp mà bạn sẽ tải lên, đảm bảo rằng những gì bạn đã kiểm thử cũng chính là những gì bạn sẽ phát hành.
Quy trình sử dụng twine
:
- Cài đặt
twine
:Bashpip install twine
- Build package của bạn:Như đã đề cập ở Phần IV, chạy lệnh sau từ thư mục gốc của dự án:Bash
python -m build
Lệnh này sẽ tạo ra các tệp.tar.gz
và.whl
trong thư mụcdist/
. - Tải lên TestPyPI:Bash
twine upload --repository testpypi dist/*
- Tải lên PyPI chính thức:Sau khi đã xác minh mọi thứ trên TestPyPI, hãy tải các tệp tương tự lên PyPI:Bash
twine upload dist/*
Khi được twine
yêu cầu thông tin đăng nhập, hãy nhập:
- Username:
__token__
- Password: Dán toàn bộ API token của bạn vào, bao gồm cả tiền tố
pypi-
.
6.3. Quy trình làm việc từ TestPyPI đến PyPI
Một quy trình làm việc hoàn chỉnh và an toàn để xuất bản một phiên bản mới của package nên tuân theo các bước sau:
- Build các bản phân phối: Chạy
python -m build
để tạo các tệpsdist
vàwheel
trong thư mụcdist/
. - Tải lên TestPyPI: Sử dụng
twine upload --repository testpypi dist/*
để tải các tệp vừa tạo lên máy chủ thử nghiệm. - Xác minh cài đặt:
- Tạo một môi trường ảo hoàn toàn mới và sạch.
- Cài đặt package của bạn từ TestPyPI bằng lệnh:Bash
pip install --index-url https://test.pypi.org/simple/ --no-deps your-package-name
(Cờ--no-deps
đảm bảo chỉ package của bạn được cài từ TestPyPI, còn các phụ thuộc khác vẫn được lấy từ PyPI chính thức).
- Kiểm tra chức năng: Trong môi trường ảo mới, chạy các bài kiểm thử hoặc thực hiện một số kiểm tra thủ công cơ bản để đảm bảo package hoạt động như mong đợi sau khi được cài đặt.
- Tải lên PyPI chính thức: Nếu tất cả các bước trên đều thành công, hãy tải chính xác các tệp đã được build và kiểm thử từ thư mục
dist/
lên PyPI chính thức bằng lệnhtwine upload dist/*
. Không được build lại package ở bước này, vì việc build lại có thể vô tình đưa vào các thay đổi không mong muốn hoặc tạo ra một sản phẩm khác với cái bạn đã kiểm thử.
Phần VII: So sánh các Công cụ Quản lý Workflow
Hệ sinh thái đóng gói Python cung cấp nhiều công cụ, điều này vừa là một thế mạnh (linh hoạt) vừa là một thách thức (gây bối rối) cho người dùng. Việc lựa chọn một bộ công cụ phù hợp với nhu cầu của dự án và quy trình làm việc của nhóm là một quyết định quan trọng. Phần này sẽ cung cấp một phân tích sâu và so sánh chi tiết giữa các công cụ quản lý workflow phổ biến để giúp bạn đưa ra lựa chọn sáng suốt.
7.1. Phân tích Sâu: Setuptools, Flit, Poetry, Hatch, PDM
Các công cụ này có thể được phân loại dựa trên phạm vi chức năng của chúng, từ các công cụ cấp thấp chỉ tập trung vào một nhiệm vụ, đến các hệ thống quản lý toàn diện.
- Setuptools (+ build + twine): Đây là cách tiếp cận nền tảng và truyền thống nhất.
setuptools
bản thân nó chủ yếu là một build backend. Nó không quản lý môi trường ảo, không giải quyết phụ thuộc, và không xử lý việc tải lên PyPI. Để có một workflow hoàn chỉnh, bạn phải kết hợp nó với các công cụ khác:venv
để tạo môi trường,pip
để cài đặt,build
để tạo các bản phân phối, vàtwine
để tải lên. Ưu điểm lớn nhất củasetuptools
là sự linh hoạt và khả năng hỗ trợ mạnh mẽ nhất cho các kịch bản build phức tạp, đặc biệt là các phần mở rộng C/C++. - Flit: Là một công cụ tối giản và có chính kiến (opinionated).
flit
được thiết kế để làm cho “những việc dễ trở nên dễ dàng”. Nó tập trung vào việc build và xuất bản các package thuần Python một cách đơn giản nhất có thể.flit
không quản lý môi trường ảo hay giải quyết phụ thuộc; bạn phải tự khai báo các phụ thuộc trongpyproject.toml
. Đây là lựa chọn tuyệt vời nếu bạn chỉ muốn nhanh chóng đưa một thư viện hoặc kịch bản đơn giản lên PyPI mà không cần các tính năng quản lý workflow phức tạp. - Poetry: Là một trong những công cụ tất-cả-trong-một (all-in-one) phổ biến nhất.
poetry
cung cấp một giải pháp tích hợp cho việc quản lý phụ thuộc, quản lý môi trường ảo, build, và xuất bản. Nó đã tiên phong trong việc sử dụng tệp khóapoetry.lock
để đảm bảo các bản cài đặt có thể tái lập một cách chính xác, giải quyết một vấn đề lớn trong hệ sinh thái. Một điểm yếu của Poetry trong quá khứ là việc chậm chạp trong việc áp dụng các tiêu chuẩn PEP mới nhất (ví dụ, nó sử dụng bảng[tool.poetry]
thay vì[project]
cho siêu dữ liệu, mặc dù điều này đang dần thay đổi) và khả năng hỗ trợ các phần mở rộng C phức tạp còn hạn chế. - Hatch: Là một công cụ tất-cả-trong-một hiện đại, có khả năng mở rộng cao và tuân thủ nghiêm ngặt các tiêu chuẩn PEP. Giống như Poetry, Hatch quản lý môi trường, phụ thuộc, build và xuất bản. Tính năng nổi bật nhất của Hatch là hệ thống quản lý môi trường mạnh mẽ, cho phép định nghĩa các ma trận kiểm thử (ví dụ: chạy bộ kiểm thử trên nhiều phiên bản Python khác nhau với các bộ phụ thuộc khác nhau). Tính năng này có thể thay thế hoàn toàn các công cụ như
tox
hoặcnox
, tích hợp quy trình kiểm thử phức tạp vào ngay trongpyproject.toml
. - PDM (Python Development Master): Là một công cụ tất-cả-trong-một hiện đại khác, cũng tuân thủ đầy đủ các tiêu chuẩn PEP. PDM nổi bật với sự hỗ trợ cho PEP 582, một đề xuất về việc lưu trữ các package trong một thư mục
__pypackages__
cục bộ thay vì môi trường ảo truyền thống (mặc dù PEP này đã bị hoãn lại). PDM cũng cung cấp một bộ tính năng toàn diện tương tự như Poetry và Hatch.
Bảng dưới đây so sánh chi tiết các công cụ này dựa trên các tính năng chính, giúp làm rõ sự khác biệt và điểm mạnh của từng công cụ.
Thuộc tính | Setuptools (+ build/twine) | Flit | Poetry | Hatch | PDM |
Trường hợp sử dụng chính | Build Backend (nền tảng) | Publisher tối giản | Quản lý workflow tất-cả-trong-một | Quản lý workflow tất-cả-trong-một | Quản lý workflow tất-cả-trong-một |
Tuân thủ PEP 621 ([project] ) | Có | Có | Không (dùng [tool.poetry] ) | Có | Có |
Giải quyết phụ thuộc | Không | Không | Có (nâng cao) | Có | Có |
Tạo tệp khóa (Lockfile) | requirements.txt (qua pip-tools ) | Không | poetry.lock | hatch.lock | pdm.lock |
Quản lý môi trường ảo | Không (dùng venv thủ công) | Không | Có (tích hợp) | Có (tích hợp) | Có (tích hợp) |
Chạy kịch bản/tác vụ | Không | Không | Có | Có (ma trận mạnh mẽ) | Có |
Hỗ trợ phần mở rộng C/C++ | Xuất sắc | Không | Kém | Kém | Kém |
Khả năng mở rộng | Plugin | Không | Plugin | Plugin | Plugin |
7.2. Khuyến nghị Lựa chọn (Decision Guide)
Không có một công cụ nào là “tốt nhất” cho mọi trường hợp. Việc lựa chọn phụ thuộc vào một sự đánh đổi giữa kiểm soát và tiện lợi.
Một cách tiếp cận theo dạng cây quyết định có thể giúp bạn định hướng:
1. Dự án của bạn có chứa các phần mở rộng cần biên dịch (C/C++/Rust) không?
- Có:
- Lựa chọn an toàn và mạnh mẽ nhất là sử dụng một build backend chuyên dụng:
setuptools
(cho C/C++),scikit-build-core
(cho CMake),meson-python
(cho Meson), hoặcmaturin
(cho Rust). - Bạn vẫn có thể sử dụng một công cụ workflow như Hatch làm “frontend” để quản lý môi trường và chạy các lệnh, trong khi giao phó nhiệm vụ build cho
setuptools
. Đây là một cách kết hợp sức mạnh của cả hai thế giới.
- Lựa chọn an toàn và mạnh mẽ nhất là sử dụng một build backend chuyên dụng:
- Không (Dự án thuần Python): Chuyển sang câu hỏi tiếp theo.
2. Mục tiêu chính của bạn là gì?
- “Tôi chỉ muốn xuất bản một thư viện/kịch bản đơn giản lên PyPI với ít phiền phức nhất.”
- Flit là một lựa chọn xuất sắc. Nó được thiết kế chính xác cho trường hợp này, loại bỏ tất cả sự phức tạp không cần thiết.
- “Tôi đang xây dựng một ứng dụng (application) hoặc một thư viện phức tạp và cần một môi trường có thể tái lập, một quy trình làm việc tích hợp để quản lý phụ thuộc, kiểm thử và xuất bản.”
- Poetry, Hatch, hoặc PDM là những lựa chọn hàng đầu.
- Chọn Poetry nếu bạn đánh giá cao hệ sinh thái trưởng thành của nó, cộng đồng người dùng lớn, và không quá bận tâm về việc nó chưa hoàn toàn tuân thủ các tiêu chuẩn mới nhất.
- Chọn Hatch nếu bạn ưu tiên sự tuân thủ nghiêm ngặt các tiêu chuẩn, muốn một công cụ có khả năng mở rộng cao, và đặc biệt quan tâm đến tính năng chạy kiểm thử theo ma trận tích hợp cho CI/CD.
- Chọn PDM nếu bạn muốn một giải pháp tuân thủ tiêu chuẩn khác, hoặc quan tâm đến các phương pháp quản lý môi trường thay thế như PEP 582.
- Poetry, Hatch, hoặc PDM là những lựa chọn hàng đầu.
Phần VIII: Những Cạm bẫy Thường gặp và Cách tránh
Ngay cả với các công cụ hiện đại, quá trình đóng gói Python vẫn còn nhiều cạm bẫy tiềm ẩn có thể gây ra lỗi và sự thất vọng. Nhận biết và tránh những sai lầm phổ biến này là chìa khóa để có một quy trình làm việc suôn sẻ và tạo ra các package chất lượng cao.
Tổng hợp các Lỗi Phổ biến trong Đóng gói
Dưới đây là danh sách các sai lầm thường gặp nhất mà các nhà phát triển Python, từ người mới bắt đầu đến người có kinh nghiệm, đều có thể mắc phải:
- Đưa các tệp kiểm thử/tài liệu vào Wheel: Đây có lẽ là lỗi phổ biến nhất. Khi thư mục
tests/
chứa một tệp__init__.py
, các công cụ nhưsetuptools.find_packages()
sẽ coi nó là một sub-package và đưa vào bản phân phối wheel. Điều này làm tăng kích thước không cần thiết của package và có thể gây ra các vấn đề về import cho người dùng cuối.- Cách tránh: Sử dụng cấu trúc
src-layout
để tách biệt mã nguồn và mã kiểm thử. Một giải pháp khác là loại bỏ các tệp__init__.py
khỏi thư mụctests/
và các thư mục con của nó.
- Cách tránh: Sử dụng cấu trúc
- Mô tả dự án không hiển thị đúng trên PyPI: Tệp
README.md
(hoặc.rst
) có lỗi cú pháp, hoặc siêu dữ liệulong_description_content_type
trongpyproject.toml
không được thiết lập hoặc thiết lập sai.- Cách tránh: Luôn chạy lệnh
twine check dist/*
trên các tệp phân phối đã được build trước khi tải chúng lên. Công cụ này sẽ xác thực xem mô tả dài có thể được hiển thị chính xác bởi PyPI hay không.
- Cách tránh: Luôn chạy lệnh
- Thiếu Sub-packages hoặc Tệp dữ liệu: Package được cài đặt thành công nhưng người dùng gặp lỗi
ImportError
vì một sub-package đã bị bỏ sót, hoặc các tệp dữ liệu (như tệp JSON, YAML, template) không được đưa vào bản phân phối. Nguyên nhân có thể là do quên thêm__init__.py
vào một thư mục con, hoặc cấu hình sai các tùy chọn nhưpackage_data
.- Cách tránh: Đảm bảo mọi thư mục con chứa mã nguồn Python đều có tệp
__init__.py
. Đối với các tệp dữ liệu, hãy sử dụng các cơ chế cấu hình dành riêng cho build backend của bạn (ví dụ:[tool.setuptools.package-data]
) hoặc sử dụng tệpMANIFEST.in
nếu backend hỗ trợ.
- Cách tránh: Đảm bảo mọi thư mục con chứa mã nguồn Python đều có tệp
- Import package trong
setup.py
: Một lỗi kinh điển từ thờisetup.py
. Việcimport your_package
bên trongsetup.py
(ví dụ, để lấy__version__
) có thể làm hỏng quá trình build, vì các phụ thuộc của package đó có thể chưa được cài đặt tại thời điểmsetup.py
được thực thi.- Cách tránh: Đọc thông tin như phiên bản trực tiếp từ tệp mã nguồn dưới dạng văn bản thay vì import module. Vấn đề này ít nghiêm trọng hơn với
pyproject.toml
vì tính chất khai báo của nó, nhưng nguyên tắc vẫn đúng: quá trình build không nên phụ thuộc vào chính package đang được build.
- Cách tránh: Đọc thông tin như phiên bản trực tiếp từ tệp mã nguồn dưới dạng văn bản thay vì import module. Vấn đề này ít nghiêm trọng hơn với
- Ghim phiên bản phụ thuộc chính xác trong một thư viện: Như đã thảo luận chi tiết ở Phần III, việc sử dụng
==
cho các phụ thuộc của một thư viện là một sai lầm nghiêm trọng, gây ra “địa ngục phụ thuộc” cho người dùng.- Cách tránh: Sử dụng các ràng buộc phiên bản linh hoạt như
~=
(khuyến nghị) hoặc>=
kết hợp với<
.
- Cách tránh: Sử dụng các ràng buộc phiên bản linh hoạt như
- Không khớp giữa tên trên PyPI và tên khi import: Đây là một nguồn gây nhầm lẫn lớn cho người dùng. Ví dụ kinh điển là
pip install pillow
nhưng lạiimport PIL
. Điều này xảy ra vì nhiều lý do lịch sử hoặc kỹ thuật.- Cách tránh: Mặc dù đôi khi không thể tránh khỏi, hãy cố gắng giữ cho tên phân phối (trong
pyproject.toml
) và tên package có thể import được càng giống nhau càng tốt. Nếu chúng khác nhau, hãy ghi chú điều này một cách thật rõ ràng và nổi bật trong tệpREADME.md
.
- Cách tránh: Mặc dù đôi khi không thể tránh khỏi, hãy cố gắng giữ cho tên phân phối (trong