Tạo Gói Thư Viện Chuẩn Python - Minh Nguyen
Có thể bạn quan tâm
Một trong những điểm mạnh của Python đó là cài đặt gói thư viện (library / package) vô cùng dễ dàng. Ta thường dùng lệnh sau để cài đặt một gói thư viện mới trong Python:
$ pip install <tên-gói-thư-viện>Bài viết sau sẽ hướng dẫn bạn các bước viết một gói thư viện chuẩn Python:
- Khai báo gói thư viện
- Cấu hình unit test
- Hiện thực thư viện Python
- Cấu hình pre-commit
- Cấu hình CI
- Viết tài liệu hướng dẫn (documentation)
Gói thư viện mẫu này mình sẽ đặt tên là blank. Toàn bộ source code sẽ được đính kèm ở cuối bài viết.
Khai báo thư viện PythonCode Python của thư viện sẽ nằm trong thư mục blank.
Hai file setup.py và setup.cfg nằm trong cùng cấp với thư mục source code.
Do đó cấu trúc thư mục sẽ trông như sau:
blank |- __init__.py |- ... setup.cfg setup.pyKhai báo thông tin gói thư viện Python trong 2 file: setup.cfg và setup.py.
setup.cfg
[metadata] name=blank download_url=https://github.com/minhng-info/blank/tarball/master description=A blank / template for creating a new Python package long_description=file:README.md long_description_content_type=text/markdown author=minhng-info author_email=minhng92@gmail.com maintainer=minhng-info maintainer_email=minhng92@gmail.com url=https://github.com/minhng-info/blank license=MIT license_files=LICENSE classifiers= Development Status :: 4 - Beta License :: OSI Approved :: MIT License Topic :: Software Development :: Libraries :: Python Modules Intended Audience :: Developers Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: Implementation [options] packages= blank python_requires=>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4, #install_requires= # <your-dependency-lib-1> # <your-dependency-lib-2> [options.entry_points] console_scripts = hello_blank = blank.main:say_hello [bdist_wheel] universal=1Giải thích một số config:
- name: tên thư viện / package.
- download_url: link download code release của thư viện. Nếu release trên Github, định dạng sẽ là: https://github.com/USER/REPOSITORY-NAME/tarball/master. Hoặc thay bằng URL khác tùy bạn.
- url: URL đến trang chủ của thư viện, ta có thể để đường dẫn của Githbu repository nếu là mã nguồn mở hoặc là trang chủ của thư viện.
- classifiers: đặc tả một vài thông tin cho thư viện như license, giai đoạn phát triển, chủ đề, version Python hỗ trợ. Tham khảo danh sách đầy đủ tại: https://pypi.org/classifiers/.
- packages: thư mục chứa source thư viện.
- python_requires: giới hạn version Python mà thư viện hỗ trợ.
- install_requires: dependency của thư viện nào vào các thư viện khác. Ví dụ nếu thư viện mình phụ thuộc vào một số thư viện khác (vd: numpy, PyYAML, …) thì mình sẽ thêm hết vào đây. Để lúc cài đặt thư viện mình nó sẽ cài các thư viện phụ thuộc vào máy người dùng, tránh bị lỗi lúc dùng thư viện của mình.
- console_scripts: nếu ta có nhu cầu cho phép người dùng chạy command line tính năng của gói thư viện chúng ta trên terminal (console) thì đặc tả theo cú pháp: <tên-câu-lệnh-trên-console> = <đường-dẫn-đến-file-python>:<tên-hàm>.
Hiện thực file setup.py:
setup.py
#!/usr/bin/env python # -*- coding: utf-8 -*- import io import re from setuptools import setup PACKAGE_NAME = "blank" with io.open("%s/__init__.py" % PACKAGE_NAME, "rt", encoding="utf8") as f: version = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1) setup(version=version)Lưu ý: cần đổi giá trị biến PACKAGE_NAME cho phù hợp.
Cấu hình unit test cho PythonTạo thư mục tests chứa tất cả unit test của thư viện chúng ta. Các unit test này sẽ tự động được chạy bởi Code Integration (CI) để đảm bảo rằng code mà developer viết phải pass tất cả unit test này. Mục đích chính tránh gây lỗi cho những tính năng cũ, cũng như đảm bảo tính ổn định cho thư viện. Thư viện hỗ trợ unit test mà mình sử dụng là pytest.
Bạn cũng sẽ cần phải tạo 1 thư mục config cho pytest, đó là conftest.py và đặt cùng cấp với thư mục source code blank. File conftest này có 2 mục đích:
- Cấu hình cho pytest, ở đây mình không có cấu hình gì đặc biệt nên ta để trống
- Code Python trong thư mục tests có thể import được source code thư viện của chúng ta là blank, nếu ta không có file này thì pytest sẽ không tìm ra thư viện để import.
Lúc này, cấu trúc thư mục ta như sau:
blank |- __init__.py |- ... tests |- test_blank.py # <- our unit test |- test_say_hello.py conftest.py setup.cfg setup.pyTrên môi trường phát triển, bạn cũng phải cài đặt pytest:
$ pip install pytest $ pytest ============================= test session starts ============================== platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 rootdir: /home/minh/Documents/GITHUB/python-blank collected 0 items ============================ no tests ran in 0.01s =============================Do chưa có gì cả, nên mình sẽ hiện thực các unit test như các file dưới đây.
- test_blank.py: kiểm tra hàm reverse trong thư viện chạy đúng hay không. Có đảo chuỗi chính xác kết quả kỳ vọng không.
- test_say_hello.py: kiểm tra entry point cho console của mình thực thi mà không bị lỗi Exception nào.
test_blank.py
import blank def test_blank_package(): my_str = "minhng.info" rev_str = blank.reverse(my_str) assert rev_str == "ofni.gnhnim"test_say_hello.py
import blank def test_say_hello(): try: blank.main.say_hello() assert True except: assert FalseThử chạy lại lệnh để kiểm tra unit test xem sao:
$ pytest =========================================== test session starts =========================================== platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 rootdir: /home/minh/Documents/GITHUB/python-blank collected 2 items tests/test_blank.py F [ 50%] tests/test_say_hello.py F [100%] ================================================ FAILURES ================================================= ___________________________________________ test_blank_package ____________________________________________ def test_blank_package(): my_str = "minhng.info" > rev_str = blank.reverse(my_str) E AttributeError: module 'blank' has no attribute 'reverse' tests/test_blank.py:5: AttributeError _____________________________________________ test_say_hello ______________________________________________ def test_say_hello(): try: > blank.main.say_hello() E AttributeError: module 'blank' has no attribute 'main' tests/test_say_hello.py:5: AttributeError During handling of the above exception, another exception occurred: def test_say_hello(): try: blank.main.say_hello() assert True except: > assert False E assert False tests/test_say_hello.py:8: AssertionError ========================================= short test summary info ========================================= FAILED tests/test_blank.py::test_blank_package - AttributeError: module 'blank' has no attribute 'reverse' FAILED tests/test_say_hello.py::test_say_hello - assert False ============================================ 2 failed in 0.03s ============================================Pytest sẽ in lỗi đầy màn hình, cái này không sao. Vì ta chưa hiện thực gì ở thư viện nên báo lỗi là đương nhiên. Nếu bạn theo phong cách Test Driven Development (TDD) thì viết test trước khi hiện thực là điều rất bình thường. Thời còn là SV thì sẽ thấy viết test chưa khi hiện thực nó hơi lạ, khác hẳn quy trình mình hay làm bài tập lớn, đó là code xong rồi mới test.
Hiện thực thư viện PythonMình sẽ hiện thực code thư viện blank để pass cả 2 unit test trên. Hiện thực 3 file trong thư mục blank đó là: __init__.py, blank_func.py và main.py.
__init__.py
from .blank_func import reverse from . import main __version__ = "0.1.0" __all__ = [ "reverse", ]Lưu ý: Ta khai báo version của gói thư viện. Khi public cập nhật phiên bản thư viện ta cần nâng cấp đánh số này lên. Version < 1.0 là trong giai đoạn Beta, còn version từ 1.0 trở lên là production - tức đảm bảo tính ổn định.
blank_func.py
def reverse(value): if not isinstance(value, str): return None result = '' for i in reversed(range(len(value))): result += value[i] return resultmain.py
def say_hello(): print("Welcome to Blank package!")Sau đó check lại unit test.
$ pytest =========================================== test session starts =========================================== platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 rootdir: /home/minh/Documents/GITHUB/python-blank collected 2 items tests/test_blank.py . [ 50%] tests/test_say_hello.py . [100%] ============================================ 2 passed in 0.02s ============================================Oh yes, pass hết test case rồi!
Cấu hình pre-commit cho gói PythonThư viện mã nguồn mở thường được tham gia phát triển bởi cộng đồng, rất nhiều developer tham gia viết source code. Do đó, ta cần đảm bảo:
- Mã nguồn cần được format chung theo 1 phong cách nhất định -> dùng Black formatter
- Mã nguồn phải pass toàn bộ unit test khi push lên remote repo.
Để làm được điều này, ta cần cấu hình pre-commit để hỗ trợ. Cấu trúc thư mục ta sẽ trông như sau:
... .pre-commit-config.yaml .flake8 pyproject.toml ...Cấu hình pre-commit
.pre-commit-config.yaml
repos: - repo: https://github.com/pre-commit/mirrors-autopep8 sha: v1.4.4 # Use the sha / tag you want to point at hooks: - id: autopep8 - repo: https://github.com/ambv/black rev: stable hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks sha: v1.2.3 hooks: - id: check-merge-conflict - id: debug-statements - id: check-yaml - id: end-of-file-fixer - id: fix-encoding-pragma - id: trailing-whitespace - id: flake8 - repo: local hooks: - id: tests name: run tests entry: pytest ./tests/ language: system files: '^tests/' types: [python]Ở đây mình đã cấu hình cho pre-commit check về: autopep8 / flake8, Black formatter style, thêm encoding, chạy test, …
.flake8
[flake8] max-line-length = 120 exclude = blank/__init__.pyCấu hình cho flake8 như trên, khi dùng bạn nhớ sửa tên blank lại.
pyproject.toml
[tool.black] py36 = true include = '\.pyi?$' exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist # The following are specific to Black, you probably don't want those. | blib2to3 | tests/data )/pyproject.toml dùng để cấu hình cho black style.
Hướng dẫn sử dụng pre-commit:
- Cài đặt pre-commit trên local: pip install pre-commit. Cấu hình pre-commit trên local, chỉ chạy ở lần đầu tiên khi mới kéo repo về máy: pre-commit install. Nếu báo lỗi pre-commit not found thì bạn có thể cài lại pre-commit bằng lệnh sudo snap install pre-commit --classic và thử lại.
- Sau khi viết code các kiểu thì bạn dùng lệnh git add để thêm file TRƯỚC khi commit.
- Chạy pre-commit bằng lệnh: pre-commit run -a
Nếu mọi thứ ok bạn có thể commit code lên repo bình thường, không thì pre-commit sẽ báo lỗi format code ở đâu đó. Do pre-commit này mình có cấu hình black formatter tự động, do đó sau khi chạy pre-commit run có thể sẽ có một số file thay đổi. Lúc này bạn cần git add lại và check lại pre-commit cho đến khi ok hết. Khi đó, code gửi lên repo đã được chuẩn hóa!
Lưu ý: Black formatter yêu cầu Python 3.6+, do đó nếu môi trường local không có bạn có thể bỏ nó đi.
Cấu hình Code Integration tự động cho gói PythonCI (Code Integration) ở đây mình sẽ hướng dẫn dùng Github Action. Tool này xuất hiện từ sau khi Microsoft mua lại Github. Nó khá tiện dụng và dễ dùng, không còn phải dùng tool ngoài như Jenkins hay Travis CI.
Github Action cho phép mình cấu hình môi trường CI, và CI Tool này sẽ chạy trên môi trường cloud. Giúp tự động hóa một số công việc như:
- Automation Test: chạy unit test và report commit đó có pass hết không.
- Tự động release phiên bản mới lên PyPI => công việc này bạn có thể quyết định tự làm manual (thủ công) hoặc dùng CI tự động như bài viết.
- Cấu hình điều kiện khi nào sẽ trigger các action này, ví dụ như trên nhánh release thì trigger action A, trên nhánh master thì trigger action B, hoặc miễn có push là trigger action C.
Cấu hình Github nằm trong thư mục .github:
... requirements.txt .github |- workflows |- pythonpackage.yml |- pythonpublish.yml ...requirements.txt: file này cần thiết để cài đặt môi trường trên CI.
setuptools pytest wheel twine.github/workflows/pythonpackage.yml
name: Python package on: [push] jobs: build: runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: python-version: [2.7, 3.5, 3.6, 3.7] steps: - uses: actions/checkout@v1 - name: Set up Python $ uses: actions/setup-python@v1 with: python-version: $ - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Lint with flake8 run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pip install pytest pip install pytest-cov pytest --cov=python_benchmark_thread_vs_process --cov-report=xml -v #- name: Upload coverage to Codecov # uses: codecov/codecov-action@v1 # with: # token: $ # file: ./coverage.xml # flags: unittests # name: codecov-umbrella # #yml: ./codecov.yml.github/workflows/pythonpublish.yml
name: Publish package to PyPI on: push: branches: - release jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: $ TWINE_PASSWORD: $ run: | python setup.py sdist bdist_wheel twine upload dist/*Thư viện của mình sẽ được publish lên PyPI khi nào có commit mới ở nhánh release (xem cấu hình trên @ on/push/branches/release). Bạn cũng có thể đọc thêm tài liệu hướng dẫn của Github Action để chỉnh sửa các file cấu hình này.
Lưu ý: hai biến PYPI_USERNAME và PYPI_PASSWORD được cấu hình tại: Settings / Secrets. Lúc này bạn có thể nhấn "Add a new secret" để tiến hành tạo các biến môi trường trên repo Github. Thông tin PYPI_USERNAME và PYPI_PASSWORD bạn phải tạo tài khoản của mình trên web PyPI.
Sau khi push các cấu hình repo này lên Github, bạn có thể kiểm tra các workflow của mình trong tab Actions.
Viết tài liệu cho thư viện - documentationỞ bước này, ta cần viết hướng dẫn sử dụng cho gói thư viện của mình tại file README.md. Bạn nên viết càng chi tiết càng tốt để có nhiều người dễ tiếp cận sử dụng hơn. Readme này được viết theo định dạng Markdown.
Tham khảo syntax cách viết documentation theo markdown tại: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
Có một số tool, thư viện hỗ trợ việc viết documentation này hoàn toàn miễn phí như sphinx. Bài viết đã khá dài rồi, bạn có thể tự mày mò tìm hiểu thêm tool khác.
.gitignore cho PythonFile .gitignore sau tương đối đầy đủ, nó sẽ bỏ qua một số file sinh ra trong quá trình chạy, giúp bạn tránh git add phải chúng.
https://github.com/minhng-info/python-blank/blob/master/.gitignore
Kiểm tra lại gói thư viện Python trên PyPIKiểm tra lại gói blank của chúng ta nhé.
$ pip install blank $ hello_blank Welcome to Blank package! $ python Python 3.5.6 |Anaconda, Inc.| (default, Aug 26 2018, 21:41:56) [GCC 7.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import blank >>> print(blank.reverse("minh")) hnimVậy là ta đã viết thành công một gói thư viện trong Python chuẩn theo phong cách mã nguồn mở!
- Blank on Github: https://github.com/minhng-info/python-blank
- Blank on PyPI: https://pypi.org/project/blank/
Các bạn cũng có thể lấy source code blank làm template để chỉnh sửa lại khi bắt đầu viết mới gói thư viện cho riêng mình.
Bài viết về Python:
- So sánh hiệu suất multi-thread và multi-process trong Python
- Tạo gói thư viện chuẩn Python
Tham gia ngay group trên Facebook để cùng thảo luận với đồng bọn nhé:
- Fanpage Minh: https://www.facebook.com/minhng.info
- Khám phá Python - PyGroup: https://www.facebook.com/groups/pygroup4u
Khám phá Python - PyGroup
Từ khóa » Thư Viện Lib Trong Python
-
Beginner Cần Biết: Top 30 Thư Viện Python Tốt Nhất (Phần 1)
-
TOP THƯ VIỆN PYTHON TỐT NHẤT - Hybrid Technologies
-
20 Thư Viện Python Bạn Không Thể Sống Thiếu Chúng - Techmaster
-
[Python Cơ Bản] Cách Import Library Trong Python - YouTube
-
Thư Viện Chuẩn Python - Tech Wiki
-
Thư Viện Python: Cái Nào Tốt Nhất Cho Vai Trò Gì? - BitDegree
-
Thư Viện Python: Cái Nào Tốt Nhất Cho Vai Trò Gì? | TopDev
-
Cách Cài Thư Viện Thường Dùng Trong Python Trên Windows
-
Cách Cài đặt Thư Viện Python - Viblo
-
Cách Viết, Đóng Gói Và Phân Phối Một Thư Viện Trong Python
-
5 Thư Viện Python Hữu Ích Cho Các Dự Án Machine Learning 2022
-
EDA With Python Library - Viblo
-
20 Thư Viện Python Thông Dụng - HocTrucTuyen123.NET
-
Top Các Thư Viện Python Sử Dụng Cho Lập Trình Trí Tuệ Nhân Tạo - Blog