Quản lý dự án Python với pyproject.toml

pyproject.toml là một file cấu hình mạnh mẽ cho các dự án Python, giúp tổ chức dự án, đặc tả bản dựng và quản lý phụ thuộc tốt hơn.

Nó được giới thiệu trong PEP 518 và đã trở thành tiêu chuẩn cho các dự án Python hiện đại, được các công cụ lớn như Poetry, Hatch, và PDM mặc định sử dụng.

Với sự hỗ trợ rộng rãi từ hệ sinh thái và cú pháp sạch, dễ đọc, pyproject.toml mang lại trải nghiệm cấu hình cải thiện đáng kể so với cách tiếp cận setup.py cũ.

Lịch sử và mục đích của pyproject.toml

Trong nhiều năm, việc đóng gói Python bị phân mảnh, dựa vào nhiều file cấu hình như setup.py, requirements.txt và các cấu hình cụ thể của công cụ.

Điều này dẫn đến sự không nhất quán, rủi ro bảo mật và thách thức bảo trì. Đặc biệt, cách tiếp cận setup.py gặp phải rủi ro thực thi mã tùy ý, các vấn đề khởi động và thiếu tiêu chuẩn hóa.

Để giải quyết những vấn đề này, PEP 518 đã giới thiệu pyproject.toml vào năm 2016, cung cấp một cách tiêu chuẩn hóa để định nghĩa các phụ thuộc bản dựng. Theo thời gian, các PEP bổ sung đã tinh chỉnh vai trò của nó, với các công cụ quan trọng như Poetry, Flit và setuptools đã áp dụng nó làm file cấu hình trung tâm.

Chuyển từ setup.py sang cách tiếp cận khai báo, pyproject.toml tăng cường bảo mật, đơn giản hóa quản lý phụ thuộc, thúc đẩy tính nhất quán và tập trung cấu hình cho các công cụ phát triển.

Bắt đầu với pyproject.toml

Bây giờ, tạo một file pyproject.toml cơ bản trong thư mục gốc của dự án:

pyproject.toml

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "pyproject-demo"
version = "0.1.0"
description = "A sample Python project using pyproject.toml"
readme = "README.md"
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
requires-python = ">=3.7"

Đoạn mã này định nghĩa các yêu cầu hệ thống bản dựng và siêu dữ liệu dự án cơ bản. Chúng ta sẽ khám phá tất cả các cách khác nhau để bạn có thể tùy chỉnh cấu hình này, nhưng bây giờ, hãy thiết lập một cấu trúc gói tối thiểu:

mkdir -p src/pyproject_demo

Tạo một file module đơn giản:

src/pyproject_demo/__init__.py

"""A sample Python package using pyproject.toml."""

__version__ = "0.1.0"


def hello():
    """Return a friendly greeting."""
    return "Hello, world!"

Bây giờ, xác minh thiết lập bằng cách cài đặt gói ở chế độ phát triển:

pip install -e .

Output:

Obtaining file:///Users/stanley/pyproject-demo
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Preparing editable metadata (pyproject.toml) ... done
  ...
  Stored in directory: /private/var/folders/rr/372_1g9j1cbd1_zhrcc13s8m0000gn/T/pip-ephem-wheel-cache-25cy86ow/wheels/17/4b/2d/a93454a53e1a643831f489eb61407df39846d1d33d89c2428a
Successfully built pyproject-demo
Installing collected packages: pyproject-demo
Successfully installed pyproject-demo-0.1.0
...

Sau khi cài đặt, ta có thể kiểm tra gói trong trình thông dịch Python. Mở shell bằng lệnh sau:

python

Sau đó, chạy đoạn mã sau:

>>> from pyproject_demo import hello
>>> hello()
'Hello, world!'

Điều đầu tiên bạn sẽ nhận thấy về pyproject.toml là nó sử dụng TOML, một định dạng file cấu hình tối giản được thiết kế để dễ đọc và viết.

Không giống như setup.py, là mã Python có thể thực thi, pyproject.toml là một đặc tả khai báo, loại bỏ nhiều vấn đề bảo mật tiềm ẩn và làm cho cấu hình dự án nhất quán hơn.

Hiểu các phần cốt lõi trong pyproject.toml

Bây giờ bạn đã thiết lập một dự án cơ bản, hãy khám phá chi tiết các phần chính của pyproject.toml. Hiểu các phần này là rất quan trọng để cấu hình hiệu quả các dự án Python.

Phần build-system

Phần [build-system] là bắt buộc theo PEP 518. Nó chỉ định các công cụ xây dựng nào được yêu cầu để xây dựng gói của bạn và backend nào sẽ sử dụng:

pyproject.toml

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

Trường requires liệt kê các gói cần thiết để xây dựng (không phải chạy) gói của bạn. build-backend chỉ định công cụ nào sẽ diễn giải các hướng dẫn xây dựng của bạn. Mặc dù setuptools là backend phổ biến nhất, các lựa chọn thay thế như Hatchling, Poetry, hoặc Flit cũng có thể được sử dụng.

Phần project

Phần [project] chứa siêu dữ liệu về gói của bạn và thay thế lệnh gọi setup() trước đây của setuptools:

pyproject.toml

[project]
name = "pyproject-demo"
version = "0.1.0"
description = "A sample Python project using pyproject.toml"
readme = "README.md"
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
requires-python = ">=3.7"
license = {text = "MIT"}
classifiers = [
    "Development Status :: 3 - Alpha",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.7",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

Phần này bao gồm tên gói, phiên bản, mô tả và các siêu dữ liệu khác sẽ được sử dụng khi xuất bản lên Python Package Index (PyPI). Trường requires-python giúp người dùng biết gói của bạn hỗ trợ các phiên bản Python nào.

Quản lý phụ thuộc

Một trong những cải tiến đáng kể nhất trong pyproject.toml là cách nó xử lý các phụ thuộc. Các phụ thuộc được chỉ định trong phần [project.dependencies]:

pyproject.toml

[project]
# ... other project metadata ...

dependencies = [
    "requests>=2.28.0",
    "pyyaml>=6.0",
    "click>=8.1.0",
]

Đối với các tính năng tùy chọn hoặc phụ thuộc phát triển, bạn có thể sử dụng phần [project.optional-dependencies]:

pyproject.toml

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=22.3.0",
    "flake8>=4.0.1",
    "mypy>=0.950",
]

docs = [
    "sphinx>=4.5.0",
    "sphinx-rtd-theme>=1.0.0",
]

Cách tiếp cận này cho phép người dùng cài đặt các phụ thuộc bổ sung chỉ khi cần:

pip install -e ".[dev]"  # Cài đặt với các phụ thuộc phát triển
pip install -e ".[docs]"  # Cài đặt với các phụ thuộc tài liệu
pip install -e ".[dev,docs]"  # Cài đặt với cả hai bộ

Điểm vào và script

Để tạo các script dòng lệnh có thể được gọi trực tiếp sau khi cài đặt, hãy sử dụng phần [project.scripts]:

pyproject.toml

[project.scripts]
pyproject-demo = "pyproject_demo.cli:main"

Cấu hình này làm cho một lệnh có tên pyproject-demo có sẵn trên đường dẫn hệ thống sau khi cài đặt, lệnh này gọi hàm main() trong module pyproject_demo.cli.

Đối với đăng ký plugin phức tạp hơn hoặc các điểm vào mà các gói khác có thể khám phá, hãy sử dụng phần [project.entry-points]:

pyproject.toml

[project.entry-points."console_scripts"]
pyproject-demo = "pyproject_demo.cli:main"

[project.entry-points."pytest11"]
pyproject-plugin = "pyproject_demo.pytest_plugin"

Cấu hình công cụ cụ thể trong pyproject.toml

Ngoài siêu dữ liệu dự án tiêu chuẩn và quản lý phụ thuộc, một trong những tính năng mạnh mẽ nhất của pyproject.toml là khả năng chứa cấu hình cho các công cụ phát triển khác nhau. Cách tiếp cận này tập trung các cài đặt dự án của bạn, loại bỏ nhu cầu về nhiều file cấu hình nằm rải rác trong thư mục dự án của bạn.

Trình định dạng mã Black

Black là một trình định dạng mã Python phổ biến, thực thi một phong cách nhất quán trên toàn bộ codebase của bạn. Bạn có thể cấu hình Black trực tiếp trong file pyproject.toml của mình:

pyproject.toml

[tool.black]
line-length = 88
target-version = ["py37", "py38", "py39"]
include = '\.pyi?

exclude = '''
/(
    \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
)/
'''

Cấu hình này đặt độ dài dòng tối đa là 88 ký tự (mặc định của Black), chỉ định các phiên bản Python mục tiêu và định nghĩa các mẫu cho các file để bao gồm hoặc loại trừ khỏi định dạng. Hỗ trợ gốc của Black cho pyproject.toml làm cho nó trở thành một ví dụ hoàn hảo về cách các công cụ Python hiện đại đang áp dụng cách tiếp cận cấu hình này.

Kiểm tra kiểu MyPy

MyPy là một trình kiểm tra kiểu tĩnh cho Python giúp bắt các lỗi liên quan đến kiểu trước khi chạy. Cấu hình của nó trong pyproject.toml rất đơn giản:

pyproject.toml

[tool.mypy]
python_version = "3.7"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false

Cấu hình này thực thi kiểm tra kiểu nghiêm ngặt cho mã dự án của bạn trong khi nới lỏng các yêu cầu cho các file kiểm thử. Tùy chọn disallow_untyped_defs đảm bảo tất cả các hàm đều có chú thích kiểu, cải thiện chất lượng mã và tài liệu. Với phần ghi đè, bạn có thể áp dụng các quy tắc khác nhau cho các module cụ thể, giúp dễ dàng áp dụng kiểm tra kiểu trong các dự án lớn hơn một cách dần dần.

Ruff

Ruff là một linter Python nhanh được viết bằng Rust nhằm thay thế nhiều công cụ như Flake8, isort, v.v. Cấu hình của nó trong pyproject.toml rất toàn diện:

pyproject.toml

[tool.ruff]
# Enable flake8-bugbear (B) rules
select = ["E", "F", "B"]
# Ignore specific rules
ignore = ["E501"]
# Line length to target
line-length = 88
# Target Python version
target-version = "py37"
# Allow autofix for all enabled rules
fixable = ["ALL"]
# Allow unused variables with leading underscore
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.isort]
known-first-party = ["pyproject_demo"]

Cấu hình này cho phép các bộ quy tắc cụ thể (E cho lỗi kiểu, F cho quy tắc flake8 và B cho quy tắc bugbear), bỏ qua các quy tắc nhất định (như E501 cho độ dài dòng) và cấu hình hành vi sắp xếp import. Cách tiếp cận thống nhất của Ruff đối với linting chứng minh giá trị của việc có một file cấu hình duy nhất cho tất cả các công cụ phát triển của bạn, vì nó có thể thay thế nhiều linter riêng biệt bằng một triển khai duy nhất, nhanh hơn.

Kết hợp nhiều công cụ

Sức mạnh thực sự của pyproject.toml đến từ việc tập trung tất cả các cấu hình này vào một nơi. Đây là cách chúng trông cùng nhau:

pyproject.toml

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
# ... project metadata ...

[tool.black]
line-length = 88
target-version = ["py37", "py38", "py39"]

[tool.mypy]
python_version = "3.7"
warn_return_any = true
disallow_untyped_defs = true

[tool.ruff]
select = ["E", "F", "B"]
line-length = 88
target-version = "py37"

Cách tiếp cận thống nhất này mang lại một số lợi thế:

  • Nguồn thông tin duy nhất: Tất cả các cấu hình nằm trong một file, giúp dễ dàng duy trì tính nhất quán.
  • Giảm tải nhận thức: Các nhà phát triển không cần nhớ nhiều định dạng và vị trí file.
  • Dễ dàng làm quen: Các thành viên nhóm mới có thể nhanh chóng hiểu thiết lập công cụ của dự án.
  • Hiệu quả kiểm soát phiên bản: Các thay đổi đối với cấu hình công cụ được theo dõi cùng nhau, đơn giản hóa việc xem xét mã.

Hầu hết các công cụ phát triển Python hiện đại đều hỗ trợ cấu hình pyproject.toml, phản ánh sự chuyển đổi rộng rãi trong cộng đồng hướng tới tiêu chuẩn hóa và đơn giản hóa.

Khi bạn phát triển các dự án Python của mình, việc tận dụng cách tiếp cận cấu hình tập trung này có thể cải thiện đáng kể quy trình làm việc phát triển và khả năng bảo trì dự án của bạn.

Định phiên bản động với pyproject.toml

Một thách thức phổ biến trong các dự án Python là duy trì số phiên bản nhất quán trên toàn bộ gói của bạn. Lặp lại phiên bản ở nhiều nơi có thể dẫn đến sự không nhất quán khi một vị trí được cập nhật nhưng những vị trí khác bị quên.

Hãy khám phá cách triển khai định phiên bản động với pyproject.toml để giải quyết vấn đề này.

Sử dụng một file phiên bản chuyên dụng

Một thực hành tốt là tạo một nguồn thông tin duy nhất cho số phiên bản của bạn:

src/pyproject_demo/_version.py

"""Version information."""

__version__ = "0.1.0"

Sau đó tham chiếu phiên bản này trong file pyproject.toml của bạn:

pyproject.toml

[project]
name = "pyproject-demo"
dynamic = ["version"]
# ... other project metadata ...

[tool.setuptools.dynamic]
version = {attr = "pyproject_demo._version.__version__"}

Với cách tiếp cận này, bạn khai báo rằng phiên bản là “động” trong phần [project] và chỉ định nguồn của nó trong phần [tool.setuptools.dynamic]. Cấu hình này cho setuptools biết đọc phiên bản từ biến __version__ trong module pyproject_demo._version tại thời điểm xây dựng.

Lợi ích chính của phương pháp này là cả mã của bạn và hệ thống xây dựng đều tham chiếu cùng một chuỗi phiên bản. Gói của bạn có thể truy cập phiên bản của nó tại thời điểm chạy bằng cách sử dụng:

from pyproject_demo._version import __version__

print(f"Running version {__version__}")

Sử dụng định phiên bản dựa trên SCM

Đối với các dự án nâng cao hơn, bạn có thể sử dụng các công cụ như setuptools-scm, tự động lấy phiên bản gói của bạn từ các thẻ git:

pyproject.toml

[build-system]
requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"

[project]
name = "pyproject-demo"
dynamic = ["version"]
# ... other project metadata ...

[tool.setuptools_scm]
write_to = "src/pyproject_demo/_version.py"

Cấu hình này thêm setuptools_scm vào các phụ thuộc xây dựng của bạn và cho nó biết ghi phiên bản (được lấy từ các thẻ git và các thay đổi cục bộ) vào một file _version.py.

Khi bạn gắn thẻ một commit trong kho lưu trữ git của mình (ví dụ: git tag -a v0.2.0 -m "Version 0.2.0"), setuptools-scm sẽ sử dụng thẻ đó làm phiên bản. Đối với phát triển cục bộ với các thay đổi chưa được commit, nó sẽ thêm các hậu tố phát triển (như .dev1+g4f8e9d1.d20230315), giúp bạn theo dõi chính xác phiên bản mã nào đang được sử dụng.

File phiên bản được tạo trong quá trình xây dựng, vì vậy nó sẽ không tồn tại trong kho lưu trữ nguồn nhưng sẽ có sẵn trong gói đã cài đặt:

>>> import pyproject_demo
>>> pyproject_demo.__version__
'0.2.0'  # Hoặc một cái gì đó như '0.2.0.dev1+g4f8e9d1.d20230315' cho các bản dựng phát triển

Quản lý phiên bản thủ công trong pyproject.toml

Đối với các dự án đơn giản hơn, bạn có thể thích duy trì phiên bản trực tiếp trong pyproject.toml:

pyproject.toml

[project]
name = "pyproject-demo"
version = "0.1.0"
# ... other project metadata ...

Tuy nhiên, bạn sẽ cần một cách để gói của bạn truy cập phiên bản này tại thời điểm chạy. Một cách tiếp cận là sử dụng module importlib.metadata (có sẵn trong Python 3.8+ hoặc thông qua backport importlib-metadata):

src/pyproject_demo/__init__.py

"""A sample Python package using pyproject.toml."""

try:
    from importlib.metadata import version, PackageNotFoundError
except ImportError:
    from importlib_metadata import version, PackageNotFoundError

try:
    __version__ = version("pyproject-demo")
except PackageNotFoundError:
    # Package is not installed
    __version__ = "unknown"


def hello():
    """Return a friendly greeting."""
    return "Hello, world!"

Phương pháp này hoạt động tốt cho các gói đã cài đặt nhưng có những hạn chế trong quá trình phát triển. Nó phù hợp nhất cho các dự án đơn giản hoặc khi không cần tích hợp với các công cụ phát triển mong đợi một số phiên bản tĩnh.

Suy nghĩ cuối cùng

Bài viết này đã khám phá cách pyproject.toml đã thay đổi việc quản lý dự án Python thông qua cấu hình khai báo, tập trung.

Việc áp dụng pyproject.toml bởi hệ sinh thái Python thể hiện cam kết tiêu chuẩn hóa mang lại lợi ích cho các nhà phát triển với bảo mật nâng cao, quy trình làm việc đơn giản hóa và quản lý phụ thuộc tốt hơn.

Hãy cân nhắc di chuyển các dự án hiện có của bạn sang pyproject.toml, bắt đầu đơn giản và dần dần kết hợp các tính năng nâng cao hơn. Quản lý dự án Python hiệu quả là một quá trình tinh chỉnh liên tục, và việc áp dụng cách tiếp cận này sẽ phục vụ bạn tốt khi các dự án của bạn phát triển.