Linting với Ruff
Install
uv pip install ruff
Tạo một file Python đơn giản với một số lỗi cố ý để có thể sửa bằng Ruff. Tạo một file tên app.py và thêm đoạn code có vấn đề sau:
#app.py
import sys
import os
import json
def add_numbers( a,b ):
"""Add two numbers together."""
result=a+b
return result
def unused_function():
"""This function is never used."""
pass
x = 10
y = 20
print(add_numbers(x,y))
Code này có một số vấn đề mà Ruff có thể phát hiện, bao gồm:
- Các import không sử dụng
- Khoảng cách không nhất quán
- Các hàm không sử dụng
- Thiếu gợi ý kiểu (type hints)
Chạy Ruff để xem nó tìm thấy gì:
ruff check app.py
Output::
app.py:1:8: F401 [*] `sys` imported but unused
|
1 | import sys
| ^^^ F401
2 | import os
3 | import json
|
= help: Remove unused import: `sys`
app.py:2:8: F401 [*] `os` imported but unused
|
1 | import sys
2 | import os
| ^^ F401
3 | import json
|
= help: Remove unused import: `os`
app.py:3:8: F401 [*] `json` imported but unused
|
1 | import sys
2 | import os
3 | import json
| ^^^^ F401
|
= help: Remove unused import: `json`
Found 3 errors.
[*] 3 fixable with the `--fix` option.
Ruff đã xác định một số vấn đề trong code và thậm chí còn chỉ ra rằng chúng có thể được sửa tự động bằng tùy chọn --fix. Hãy sửa chúng:
ruff check --fix app.py
Output::
Found 3 errors (3 fixed, 0 remaining).
Chạy lệnh này kích hoạt Ruff sửa tất cả các lỗi:
#app.py
def add_numbers(a, b):
"""Add two numbers together."""
result = a + b
return result
def unused_function():
"""This function is never used."""
pass
x = 10
y = 20
print(add_numbers(x, y))
Ruff đã loại bỏ các import không sử dụng, sửa khoảng cách xung quanh các toán tử và dấu phẩy, và định dạng đúng các tham số hàm để dễ đọc hơn.
Vấn đề duy nhất nó không giải quyết là hàm không sử dụng, vì nó không thể xác định liệu đó có phải là cố ý hay không.
Cấu hình Ruff
Với các vấn đề ban đầu trong app.py đã được sửa, ta có thể tự hỏi liệu còn gì để cấu hình không. Mặc dù Ruff hoạt động tốt ngay từ đầu, việc tinh chỉnh các cài đặt của nó đảm bảo nó phù hợp với phong cách và quy ước cụ thể của dự án.
Tạo một file pyproject.toml trong thư mục gốc của dự án:
touch pyproject.toml
Mở file pyproject.toml trong trình soạn thảo văn bản và bắt đầu với một cấu hình cơ bản:
pyproject.toml
[tool.ruff]
# Enable basic checks
lint.select = ["E", "F"]
Cấu hình này cho phép:
E: Lỗi kiểu từ pycodestyleF: Lỗi logic và cú pháp từ pyflakes
Bây giờ, chạy Ruff một lần nữa trên code đã được sửa:
ruff check app.py
Output::
All checks passed!
Không có vấn đề nào được báo cáo, vì đã sửa các lỗi kiểu và logic cơ bản.
Hãy xem điều gì xảy ra khi cố gắng kiểm tra các vấn đề sắp xếp import.
Đầu tiên, sửa đổi app.py để thêm các import được sử dụng nhưng không được sắp xếp:
#app.py
import datetime
from pathlib import Path
import math
from collections import defaultdict
import json
def add_numbers(a, b):
"""Add two numbers together."""
result = a + b
return result
def get_current_time():
"""Get the current time."""
return datetime.datetime.now()
def read_config():
"""Read configuration from a JSON file."""
config_path = Path("config.json")
if config_path.exists():
return json.loads(config_path.read_text())
return {}
def calculate_stats(values):
"""Calculate statistics for a list of values."""
stats = defaultdict(int)
stats["sum"] = sum(values)
stats["average"] = stats["sum"] / len(values)
stats["sqrt_sum"] = math.sqrt(stats["sum"])
return stats
x = 10
y = 20
print(add_numbers(x, y))
print(get_current_time())
print(calculate_stats([1, 2, 3, 4, 5]))
Chạy Ruff với cấu hình hiện tại:
ruff check app.py
Output::
All checks passed!
Ruff không báo cáo bất kỳ vấn đề nào với các import, mặc dù chúng không được sắp xếp. Đó là vì chưa bật các quy tắc sắp xếp import.
Cập nhật cấu hình:
pyproject.toml
[tool.ruff]
# Enable basic checks and import sorting
lint.select = ["E", "F", "I"] # Added "I" for import sorting
Chạy lại Ruff:
ruff check app.py
Một trong những vấn đề đầu tiên Ruff sẽ gắn cờ là liên quan đến các import không được sắp xếp:
Output::
app.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import datetime
2 | | from pathlib import Path
3 | | import math
4 | | from collections import defaultdict
5 | | import json
| |___________^ I001
|
= help: Organize imports
Found 1 error.
[*] 1 fixable with the `--fix` option.
Bây giờ Ruff báo cáo một vấn đề mới: I001, cho biết các import không được sắp xếp.
ruff check --fix app.py
Output::
Found 1 error (1 fixed, 0 remaining).
Sau khi chạy lệnh này, app.py sẽ trông như thế này:
#app.py
import datetime
import json
import math
from collections import defaultdict
from pathlib import Path
...
Lưu ý cách Ruff đã sắp xếp đúng các import:
- Các import thư viện chuẩn trước (
datetime,json,math) - Các import bên thứ ba (không có trong ví dụ này)
- Các import bên thứ nhất và import tương đối cuối cùng (
collections,pathlib) - Tất cả các import được sắp xếp theo thứ tự bảng chữ cái trong các nhóm
- Các câu lệnh import (
import x) đứng trước các from-import (from x import y)
Thêm quy tắc Bugbear để phát hiện lỗi tốt hơn
Hãy cải thiện cấu hình để bắt các lỗi tinh vi hơn và các vấn đề thiết kế. Bộ quy tắc “B” từ Bugbear giúp xác định các cạm bẫy phổ biến mà các linter khác có thể bỏ qua, chẳng hạn như các đối số mặc định có thể thay đổi, các biến vòng lặp không sử dụng và các so sánh dư thừa.
pyproject.toml
[tool.ruff]
# Add Bugbear rules for catching bugs and design problems
lint.select = ["E", "F", "I", "B"]
Bây giờ, sửa đổi app.py để bao gồm các vấn đề mà quy tắc Bugbear sẽ phát hiện:
#app.py
import datetime
import json
import math
from collections import defaultdict
from pathlib import Path
# Existing functions remain unchanged
...
def process_items(items=[]): # Bugbear will flag mutable default argument
"""Process a list of items."""
return [x for x in items for y in items] # Bugbear will flag nested comprehension
x = 10
y = 20
print(add_numbers(x, y))
print(get_current_time())
print(calculate_stats([1, 2, 3, 4, 5]))
Chạy Ruff để xem nó phát hiện những vấn đề gì:
ruff check app.py
Output:
app.py:36:25: B006 Do not use mutable data structures for argument defaults
|
36 | def process_items(items=[]): # Bugbear will flag mutable default argument
| ^^ B006
37 | """Process a list of items."""
38 | return [x for x in items for y in items] # Bugbear will flag nested comprehension
|
= help: Replace with `None`; initialize within function
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
Vấn đề đầu tiên, B006, cảnh báo về các đối số mặc định có thể thay đổi. Python khởi tạo chúng một lần tại định nghĩa hàm, khiến tất cả các lệnh gọi chia sẻ cùng một thể hiện, điều này có thể dẫn đến hành vi không mong muốn.
Vấn đề thứ hai, B007, gắn cờ một biến vòng lặp không sử dụng trong biểu thức list comprehension lồng nhau. Biến vòng lặp thứ hai y không được sử dụng, cho thấy một lỗi tiềm ẩn hoặc hiểu lầm về các vòng lặp lồng nhau.
Bây giờ hãy sửa các vấn đề với các thực hành code hóa tốt hơn:
#app.py
import datetime
import json
import math
from collections import defaultdict
from pathlib import Path
# Existing functions remain unchanged
...
def process_items(items=None): # Fixed: use None instead of mutable default
"""Process a list of items."""
if items is None:
items = []
return [x for x in items] # Fixed: simplified comprehension
# Rest of the code remains unchanged
...
Output:
All checks passed!
Type annotations
Chú thích cải thiện khả năng đọc code và giúp bắt các lỗi liên quan đến kiểu sớm. Thêm bộ quy tắc “ANN” vào cấu hình đảm bảo code được chú thích kiểu đúng cách:
pyproject.toml
[tool.ruff]
# Add type annotation rules
lint.select = ["E", "F", "I", "B", "ANN"]
Với cấu hình này, Ruff sẽ kiểm tra các kiểu tham số bị thiếu, kiểu trả về và các vấn đề chú thích khác. Hãy chạy nó trên app.py của chúng ta:
ruff check app.py
Output:
...
app.py:7:1: ANN001 [*] Missing type annotation for function argument `a`
app.py:7:1: ANN001 [*] Missing type annotation for function argument `b`
app.py:7:1: ANN201 [*] Missing return type annotation for function
...
app.py:29:1: ANN001 [*] Missing type annotation for function argument `items`
app.py:29:1: ANN201 [*] Missing return type annotation for function
Found 9 errors.
[*] 9 fixable with the `--fix` option.
Chú thích đóng vai trò là tài liệu, giúp dễ dàng hiểu một hàm mong đợi gì và trả về gì. Chúng cũng cho phép hỗ trợ IDE tốt hơn và cho phép các công cụ như MyPy thực hiện kiểm tra kiểu tĩnh. Hãy thêm chú thích vào một số hàm của chúng ta:
#app.py
import datetime
import json
import math
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Optional
def add_numbers(a: float, b: float) -> float:
"""Add two numbers together."""
result = a + b
return result
def get_current_time() -> datetime.datetime:
....
def read_config() -> Dict[str, Any]:
...
def calculate_stats(values: List[float]) -> Dict[str, float]:
...
def process_items(items: Optional[List[str]] = None) -> List[str]:
...
Bây giờ, mỗi hàm định nghĩa rõ ràng các kiểu đầu vào và trả về mong đợi.
Chạy Ruff một lần nữa để đảm bảo đã giải quyết tất cả các vấn đề chú thích kiểu:
ruff check app.py
Ruff sẽ không còn phàn nàn về việc thiếu chú thích kiểu:
Output:
All checks passed!
Tùy chỉnh độ dài dòng và bỏ qua file
Khi dự án phát triển, ta có thể muốn tùy chỉnh hành vi của Ruff để phù hợp với sở thích. Đặt độ dài dòng tùy chỉnh có thể cần thiết nếu muốn các dòng dài hơn một chút so với mặc định, và loại trừ các thư mục ngăn Ruff lãng phí thời gian kiểm tra các file không nên được lint.
pyproject.toml
[tool.ruff]
select = ["E", "F", "I", "B", "ANN"]
line-length = 88
# Exclude directories
exclude = [
".git",
".mypy_cache",
".ruff_cache",
"venv",
"__pycache__",
]
# Target Python version
target-version = "py312"
Cấu hình này duy trì độ dài dòng mặc định được khuyến nghị nhưng ta có thể thay đổi nó nếu cần.
Nó cũng loại trừ các thư mục phổ biến như môi trường ảo và thư mục cache, và đặt Python 3.12 làm phiên bản mục tiêu.
Ruff sẽ chỉ đề xuất các tính năng và sửa lỗi tương thích với Python 3.12, đảm bảo code vẫn tương thích với môi trường runtime .
Bỏ qua quy tắc theo từng file
Các phần khác nhau của codebase có thể yêu cầu các quy tắc linting khác nhau. Ví dụ, các file __init__.py thường chứa các import không sử dụng có chủ ý được dùng để re-export các symbol, và các file kiểm thử có thể không cần chú thích kiểu.
Bạn có thể cấu hình các ngoại lệ này bằng cách bỏ qua quy tắc theo từng file:
pyproject.toml
[tool.ruff]
select = ["E", "F", "I", "B", "ANN"]
line-length = 88
exclude = [
".git",
".mypy_cache",
".ruff_cache",
"venv",
"__pycache__",
]
target-version = "py312"
[[tool.ruff.lint.per-file-ignores]]
# Ignore unused imports in __init__.py files
"__init__.py" = ["F401"]
# Ignore missing type annotations in tests
"test_*.py" = ["ANN"]
Cách tiếp cận này cho phép duy trì các tiêu chuẩn nghiêm ngặt trên toàn bộ codebase chính của mình trong khi vẫn đáp ứng các trường hợp đặc biệt mà không ảnh hưởng đến chất lượng code tổng thể.
Sử dụng Ruff làm trình định dạng code
Ngoài linting, Ruff cũng có thể tự động định dạng code. Trình định dạng của Ruff được thiết kế để tương thích với Black, trình định dạng Python phổ biến, đồng thời mang lại hiệu suất tốt hơn đáng kể.
Sử dụng Ruff cho cả linting và định dạng giúp đơn giản hóa chuỗi công cụ và đảm bảo phong cách code nhất quán trên toàn bộ dự án.
Trình định dạng được gọi bằng lệnh format:
ruff format filename.py
Tạo một file mới bằng trình soạn thảo dòng lệnh để tránh tự động thụt lề cho ví dụ này. Thêm đoạn code sau với một số vấn đề định dạng để xem trình định dạng của Ruff hoạt động như thế nào:
messy.py
def add_numbers(a,b, c):
return a+b+c
x = { 'key1' :42,'key2': 100 }
Code có nhiều vấn đề định dạng, bao gồm khoảng cách không nhất quán xung quanh dấu phẩy, toán tử và dấu hai chấm, định dạng từ điển không đúng với khoảng cách không đều, thiếu dòng trống giữa các định nghĩa và dấu nháy đơn thay vì dấu nháy kép cho chuỗi.
Bây giờ, chạy trình định dạng của Ruff trên file này:
ruff format messy.py
Sau khi định dạng, file messy.py sẽ trông sạch sẽ hơn nhiều:
messy.py (sau khi định dạng)
def add_numbers(a, b, c):
return a + b + c
x = {"key1": 42, "key2": 100}
Ruff đã tự động sửa tất cả các vấn đề này:
- Thêm khoảng cách nhất quán sau dấu phẩy trong các tham số hàm
- Thêm khoảng cách xung quanh các toán tử trong biểu thức trả về
- Thêm một dòng trống giữa hàm và gán biến
- Tiêu chuẩn hóa định dạng từ điển không có khoảng trắng sau { hoặc trước }
- Chuyển đổi dấu nháy đơn thành dấu nháy kép cho chuỗi
- Thêm khoảng cách nhất quán xung quanh dấu hai chấm trong các cặp khóa-giá trị
Những cải tiến định dạng này làm cho code dễ đọc và nhất quán hơn, tuân theo các thực hành tốt nhất của Python như PEP 8.
Một trong những tính năng mạnh nhất của Ruff là khả năng vừa lint vừa định dạng code. Thay vì sử dụng nhiều công cụ (như Flake8 để linting và Black để định dạng), ta có thể đơn giản hóa chuỗi công cụ của mình chỉ với Ruff:
ruff check app.py && ruff format app.py
Output:
All checks passed!
1 file left unchanged
Kết hợp các lệnh này trong quy trình làm việc mang lại lợi ích của việc linting toàn diện và định dạng nhất quán trong một công cụ duy nhất.
Tích hợp Ruff với pre-commit hooks
Sau khi cấu hình Ruff cho linting và định dạng, bước tiếp theo là tự động hóa các kiểm tra này trong quy trình phát triển .
Pre-commit hooks cung cấp một cách tuyệt vời để đảm bảo các tiêu chuẩn chất lượng code được đáp ứng trước mỗi commit.
Vì thư mục dự án hiện không phải là một kho lưu trữ Git, ta sẽ cần khởi tạo một kho lưu trữ trước:
git init
Tiếp theo, tạo một file .gitignore để loại trừ các file không cần thiết khỏi kiểm soát phiên bản:
.gitignore
# Virtual environment
venv/
# Python cache files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
# Ruff cache
.ruff_cache/
# Other common exclusions
.DS_Store
Với kho lưu trữ Git đã được khởi tạo, bây giờ ta có thể thiết lập pre-commit hooks:
pip install pre-commit
Tạo một file cấu hình pre-commit tên .pre-commit-config.yaml để tích hợp Ruff cho cả linting và định dạng:
.pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Cấu hình này định nghĩa hai hook:
ruff: Chạy linter với các sửa lỗi tự động được bậtruff-format: Áp dụng định dạng cho code
Cài đặt các hook vào kho lưu trữ Git :
pre-commit install
Ta sẽ thấy đầu ra xác nhận việc cài đặt:
Output:
pre-commit installed at .git/hooks/pre-commit
Đầu tiên, xóa tất cả code trong app.py để xác minh rằng thiết lập hoạt động đúng. Sau đó, thêm một số vi phạm quy tắc để kiểm tra cấu hình :
#app.py
import math
import json
import datetime # Unsorted imports
def problematic_function( x,y): # Spacing issues
result=x+y # Missing spaces around operator
return result
Bây giờ hãy thêm các thay đổi và thử commit chúng:
git add app.py
git commit -m "Test pre-commit hooks"
Các pre-commit hooks sẽ tự động chạy và sửa các vấn đề:
Output:
[INFO] Initializing environment for https://github.com/astral-sh/ruff-pre-commit.
[INFO] Installing environment for https://github.com/astral-sh/ruff-pre-commit.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
ruff.....................................................................Failed
- hook id: ruff
- exit code: 1
- files were modified by this hook
app.py:3:5: ANN201 Missing return type annotation for public function `problematic_function`
app.py:3:26: ANN001 Missing type annotation for function argument `x`
app.py:3:29: ANN001 Missing type annotation for function argument `y`
Found 6 errors (3 fixed, 3 remaining).
ruff-format..............................................................Failed
- hook id: ruff-format
- files were modified by this hook
1 file reformatted
Trong khi Ruff đã sửa một số vấn đề (như khoảng cách và định dạng), nó vẫn báo cáo lỗi liên quan đến thiếu chú thích kiểu, yêu cầu sửa thủ công.
Để sửa các vấn đề đó, hãy thêm các chú thích kiểu cần thiết:
#app.py
def problematic_function(x: float, y: float) -> float: # Added type annotations
result = x + y
return result
Thêm các thay đổi một lần nữa:
git add app.py
git commit -m "Test pre-commit hooks"
Output:
ruff.....................................................................Passed
ruff-format..............................................................Passed
[master (root-commit) f1926e6] Test pre-commit hooks
1 file changed, 3 insertions(+)
Lần này, commit sẽ thành công.
Trong một số trường hợp, ta có thể cần tạm thời bỏ qua các pre-commit hooks:
git commit -m "Emergency fix" --no-verify
Tuy nhiên, điều này chỉ nên được sử dụng một cách tiết kiệm và chỉ trong những trường hợp đặc biệt.